< prev index next >

application/org.openjdk.jmc.ui/src/main/java/org/openjdk/jmc/ui/charts/XYChart.java

Print this page




  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.Graphics2D;
  37 import java.awt.Point;
  38 import java.awt.geom.AffineTransform;
  39 import java.awt.geom.Point2D;
  40 import java.util.ArrayList;
  41 import java.util.HashSet;
  42 import java.util.List;
  43 import java.util.Set;

  44 import java.util.function.Consumer;
  45 
  46 import org.openjdk.jmc.common.IDisplayable;
  47 import org.openjdk.jmc.common.unit.IQuantity;
  48 import org.openjdk.jmc.common.unit.IRange;
  49 import org.openjdk.jmc.common.unit.QuantitiesToolkit;
  50 import org.openjdk.jmc.common.unit.QuantityRange;

  51 import org.openjdk.jmc.ui.charts.IChartInfoVisitor.ITick;



  52 
  53 public class XYChart {
  54         private static final String ELLIPSIS = "..."; //$NON-NLS-1$
  55         private static final Color SELECTION_COLOR = new Color(255, 255, 255, 220);
  56         private static final Color RANGE_INDICATION_COLOR = new Color(255, 60, 20);
  57         private static final int Y_OFFSET = 35;
  58         private static final int RANGE_INDICATOR_HEIGHT = 4;
  59         private final IQuantity start;
  60         private final IQuantity end;

  61         private IXDataRenderer rendererRoot;
  62         private IRenderedRow rendererResult;
  63         private final int xOffset;

  64         private final int bucketWidth;
  65         // FIXME: Use bucketWidth * ticksPerBucket instead of hardcoded value?
  66 //      private final int ticksPerBucket = 4;
  67 
  68         private IQuantity currentStart;
  69         private IQuantity currentEnd;
  70 
  71         private final Set<Object> selectedRows = new HashSet<>();


  72         private IQuantity selectionStart;
  73         private IQuantity selectionEnd;
  74         private SubdividedQuantityRange xBucketRange;
  75         private SubdividedQuantityRange xTickRange;
  76         private int axisWidth;










  77 
  78         public XYChart(IRange<IQuantity> range, IXDataRenderer rendererRoot) {
  79                 this(range.getStart(), range.getEnd(), rendererRoot);
  80         }
  81 
  82         public XYChart(IRange<IQuantity> range, IXDataRenderer rendererRoot, int xOffset) {
  83                 this(range.getStart(), range.getEnd(), rendererRoot, xOffset);
  84         }
  85 












  86         public XYChart(IRange<IQuantity> range, IXDataRenderer rendererRoot, int xOffset, int bucketWidth) {
  87                 this(range.getStart(), range.getEnd(), rendererRoot, xOffset, bucketWidth);
  88         }
  89 
  90         public XYChart(IQuantity start, IQuantity end, IXDataRenderer rendererRoot) {
  91                 this(start, end, rendererRoot, 60);
  92         }
  93 
  94         public XYChart(IQuantity start, IQuantity end, IXDataRenderer rendererRoot, int xOffset) {
  95                 this(start, end, rendererRoot, xOffset, 25);
  96         }
  97 
  98         public XYChart(IQuantity start, IQuantity end, IXDataRenderer rendererRoot, int xOffset, int bucketWidth) {
  99                 this.rendererRoot = rendererRoot;
 100                 // Start value must always be strictly less than end
 101                 assert (start.compareTo(end) < 0);
 102                 currentStart = start;
 103                 this.start = start;
 104                 currentEnd = end;
 105                 this.end = end;
 106                 this.xOffset = xOffset;
 107                 this.bucketWidth = bucketWidth;
 108         }
 109 
 110         public void setRendererRoot(IXDataRenderer rendererRoot) {
 111                 clearSelection();
 112                 this.rendererRoot = rendererRoot;
 113         }
 114 
 115         public IXDataRenderer getRendererRoot() {
 116                 return rendererRoot;
 117         }
 118 
 119         public Object[] getSelectedRows() {
 120                 return selectedRows.toArray(new Object[selectedRows.size()]);
 121         }
 122 
 123         public IQuantity getSelectionStart() {
 124                 return selectionStart;
 125         }
 126 
 127         public IQuantity getSelectionEnd() {
 128                 return selectionEnd;
 129         }
 130 
 131         public IRange<IQuantity> getSelectionRange() {
 132                 return (selectionStart != null) && (selectionEnd != null)
 133                                 ? QuantityRange.createWithEnd(selectionStart, selectionEnd) : null;
 134         }
 135 
 136         public void render(Graphics2D context, int width, int height) {
 137                 if (width > xOffset && height > Y_OFFSET) {
 138                         axisWidth = width - xOffset;
 139                         // FIXME: xBucketRange and xTickRange should be more related, so that each tick is typically an integer number of buckets (or possibly 2.5 buckets).
 140                         xBucketRange = new SubdividedQuantityRange(currentStart, currentEnd, axisWidth, bucketWidth);
 141                         // FIXME: Use bucketWidth * ticksPerBucket instead of hardcoded value?
 142                         xTickRange = new SubdividedQuantityRange(currentStart, currentEnd, axisWidth, 100);
 143                         AffineTransform oldTransform = context.getTransform();
 144                         context.translate(xOffset, 0);
 145                         doRender(context, height - Y_OFFSET);
 146                         context.setTransform(oldTransform);
 147                 }
 148         }
 149 

















 150         private void renderRangeIndication(Graphics2D context, int rangeIndicatorY) {
 151                 // FIXME: Extract the needed functionality from SubdividedQuantityRange
 152                 SubdividedQuantityRange fullRangeAxis = new SubdividedQuantityRange(start, end, axisWidth, 25);
 153                 int x1 = (int) fullRangeAxis.getPixel(currentStart);
 154                 int x2 = (int) Math.ceil(fullRangeAxis.getPixel(currentEnd));
 155                 if (x1 > 0 || x2 < axisWidth) {



 156                         context.setPaint(RANGE_INDICATION_COLOR);
 157                         context.fillRect(x1, rangeIndicatorY, x2 - x1, RANGE_INDICATOR_HEIGHT);
 158                         context.setPaint(Color.DARK_GRAY);
 159                         context.drawRect(0, rangeIndicatorY, axisWidth - 1, RANGE_INDICATOR_HEIGHT);
 160                 }
 161         }
 162 
 163         private void doRender(Graphics2D context, int axisHeight) {

 164                 context.setPaint(Color.LIGHT_GRAY);
 165                 AWTChartToolkit.drawGrid(context, xTickRange, axisHeight, false);
 166                 // Attempt to make graphs so low they cover the axis show by drawing the full axis first ...
 167                 context.setPaint(Color.BLACK);



 168                 AWTChartToolkit.drawAxis(context, xTickRange, axisHeight - 1, false, 1 - xOffset, false);

 169                 // ... then the graph ...
 170                 rendererResult = rendererRoot.render(context, xBucketRange, axisHeight);
 171                 AffineTransform oldTransform = context.getTransform();
 172                 renderText(context, rendererResult);
 173                 context.setTransform(oldTransform);
 174                 if (!selectedRows.isEmpty()) {
 175                         renderSelection(context, rendererResult);
 176                         context.setTransform(oldTransform);
 177                 }
 178                 // .. and finally a semitransparent axis line again.
 179                 context.setPaint(new Color(0, 0, 0, 64));
 180                 context.drawLine(0, axisHeight - 1, axisWidth - 1, axisHeight - 1);
 181                 renderRangeIndication(context, axisHeight + 25);
 182         }
 183 
 184         private void renderSelection(Graphics2D context, IRenderedRow row) {









































 185                 if (selectedRows.contains(row.getPayload())) {
 186                         renderSelection(context, xBucketRange, row.getHeight());
 187                 } else {
 188                         List<IRenderedRow> subdivision = row.getNestedRows();
 189                         if (subdivision.isEmpty()) {
 190                                 dimRect(context, 0, axisWidth, row.getHeight());
 191                         } else {
 192                                 for (IRenderedRow nestedRow : row.getNestedRows()) {
 193                                         renderSelection(context, nestedRow);
 194                                 }
 195                                 return;
 196                         }
 197                 }
 198                 context.translate(0, row.getHeight());
 199         }
 200 














 201         private void renderText(Graphics2D context, IRenderedRow row) {
 202                 String text = row.getName();
 203                 int height = row.getHeight();
 204                 if (height >= context.getFontMetrics().getHeight()) {
 205                         if (text != null) {

 206                                 context.setColor(Color.BLACK);
 207                                 int y;
 208                                 if (height > 40) {
 209                                         context.drawLine(-xOffset, height - 1, -15, height - 1);
 210                                         y = height - context.getFontMetrics().getHeight() / 2;
 211                                 } else {
 212                                         // draw the string in the middle of the row
 213                                         y = ((height - context.getFontMetrics().getHeight()) / 2) + context.getFontMetrics().getAscent();
 214                                 }
 215                                 int charsWidth = context.getFontMetrics().charsWidth(text.toCharArray(), 0, text.length());
 216                                 if (charsWidth > xOffset) {
 217                                         float fitRatio = ((float) xOffset) / (charsWidth
 218                                                         + context.getFontMetrics().charsWidth(ELLIPSIS.toCharArray(), 0, ELLIPSIS.length()));
 219                                         text = text.substring(0, ((int) (text.length() * fitRatio)) - 1) + ELLIPSIS;
 220                                 }
 221                                 context.drawString(text, -xOffset + 2, y);
 222                         } else {
 223                                 List<IRenderedRow> subdivision = row.getNestedRows();
 224                                 if (!subdivision.isEmpty()) {
 225                                         for (IRenderedRow nestedRow : row.getNestedRows()) {
 226                                                 renderText(context, nestedRow);
 227                                         }
 228                                         return;
 229                                 }
 230                         }
 231                 }
 232                 context.translate(0, height);
 233         }
 234 
 235         /**
 236          * Pan the view.
 237          *
 238          * @param rightPercent
 239          * @return true if the bounds changed. That is, if a redraw is required.
 240          */
 241         public boolean pan(int rightPercent) {



 242                 if (xBucketRange != null) {
 243                         IQuantity oldStart = currentStart;
 244                         IQuantity oldEnd = currentEnd;
 245                         if (rightPercent > 0) {
 246                                 currentEnd = QuantitiesToolkit
 247                                                 .min(xBucketRange.getQuantityAtPixel(axisWidth + axisWidth * rightPercent / 100), end);
 248                                 currentStart = QuantitiesToolkit
 249                                                 .max(xBucketRange.getQuantityAtPixel(xBucketRange.getPixel(currentEnd) - axisWidth), start);
 250                         } else if (rightPercent < 0) {
 251                                 currentStart = QuantitiesToolkit.max(xBucketRange.getQuantityAtPixel(axisWidth * rightPercent / 100),
 252                                                 start);
 253                                 currentEnd = QuantitiesToolkit
 254                                                 .min(xBucketRange.getQuantityAtPixel(xBucketRange.getPixel(currentStart) + axisWidth), end);
 255                         }
 256                         return (currentStart.compareTo(oldStart) != 0) || (currentEnd.compareTo(oldEnd) != 0);
 257                 }
 258                 // Return true since a redraw forces creation of xBucketRange.
 259                 return true;
 260         }
 261 
 262         /**



































 263          * Zoom the view.
 264          *
 265          * @param zoomInSteps
 266          * @return true if the bounds changed. That is, if a redraw is required.
 267          */
 268         public boolean zoom(int zoomInSteps) {



 269                 return zoomXAxis(axisWidth / 2, zoomInSteps);
 270         }
 271 
 272         /**
 273          * Zoom the view.
 274          *
 275          * @param x
 276          * @param zoomInSteps
 277          * @return true if the bounds changed. That is, if a redraw is required.
 278          */
 279         public boolean zoom(int x, int zoomInSteps) {
 280                 return zoomXAxis(x - xOffset, zoomInSteps);
 281         }
 282 

 283         private boolean zoomXAxis(int x, int zoomInSteps) {
 284                 if (xBucketRange == null) {
 285                         // Return true since a redraw forces creation of xBucketRange.
 286                         return true;
 287                 }
 288                 if ((x > 0) && (x < axisWidth)) {
 289                         IQuantity oldStart = currentStart;
 290                         IQuantity oldEnd = currentEnd;
 291                         // Absolute value of zoomFactor must be less than 1. Currently it ranges between -0.5 and 0.5.
 292                         double zoomFactor = Math.atan(zoomInSteps) / Math.PI;
 293                         int newStart = (int) (zoomFactor * x);
 294                         int newEnd = (int) (axisWidth * (1 - zoomFactor)) + newStart;
 295                         SubdividedQuantityRange xAxis = new SubdividedQuantityRange(currentStart, currentEnd, axisWidth, 1);
 296                         setVisibleRange(xAxis.getQuantityAtPixel(newStart), xAxis.getQuantityAtPixel(newEnd));
 297                         return (currentStart.compareTo(oldStart) != 0) || (currentEnd.compareTo(oldEnd) != 0);
 298                 }
 299                 return false;
 300         }
 301 












































































































































 302         public void setVisibleRange(IQuantity rangeStart, IQuantity rangeEnd) {




 303                 rangeStart = QuantitiesToolkit.max(rangeStart, start);
 304                 rangeEnd = QuantitiesToolkit.min(rangeEnd, end);
 305                 if (rangeStart.compareTo(rangeEnd) < 0) {
 306                         SubdividedQuantityRange testRange = new SubdividedQuantityRange(rangeStart, rangeEnd, 10000, 1);
 307                         if (testRange.getQuantityAtPixel(0).compareTo(testRange.getQuantityAtPixel(1)) < 0) {
 308                                 currentStart = rangeStart;
 309                                 currentEnd = rangeEnd;
 310                         } else {
 311                                 // Ensures that zoom out is always allowed
 312                                 currentStart = QuantitiesToolkit.min(rangeStart, currentStart);
 313                                 currentEnd = QuantitiesToolkit.max(rangeEnd, currentEnd);
 314                         }




 315                         rangeListeners.stream().forEach(l -> l.accept(getVisibleRange()));
 316                 }
 317         }
 318 
 319         private List<Consumer<IRange<IQuantity>>> rangeListeners = new ArrayList<>();
 320 
 321         public void addVisibleRangeListener(Consumer<IRange<IQuantity>> rangeListener) {
 322                 rangeListeners.add(rangeListener);
 323         }
 324 
 325         public IRange<IQuantity> getVisibleRange() {
 326                 return (currentStart != null) && (currentEnd != null) ? QuantityRange.createWithEnd(currentStart, currentEnd)
 327                                 : null;
 328         }
 329 
 330         public void clearVisibleRange() {
 331                 currentStart = start;
 332                 currentEnd = end;
 333         }
 334 
 335         public boolean select(int x1, int x2, int y1, int y2) {
 336                 int xStart = Math.min(x1, x2) - xOffset;
 337                 int xEnd = Math.max(x1, x2) - xOffset;
 338 
 339                 if (xBucketRange != null && (xEnd >= 0)) {
 340                         return select(xBucketRange.getQuantityAtPixel(Math.max(0, xStart)), xBucketRange.getQuantityAtPixel(xEnd),
 341                                         y1, y2);
 342                 } else {
 343                         return select(null, null, y1, y2);
 344                 }
 345         }
 346 
 347         public boolean select(IQuantity xStart, IQuantity xEnd, int y1, int y2) {
 348                 if (xStart != null && xStart.compareTo(start) < 0) {
 349                         xStart = start;
 350                 }
 351                 if (xEnd != null && xEnd.compareTo(end) > 0) {
 352                         xEnd = end;
 353                 }
 354                 Set<Object> oldRows = null;
 355                 if (QuantitiesToolkit.same(selectionStart, xStart) && QuantitiesToolkit.same(selectionEnd, xEnd)) {
 356                         oldRows = new HashSet<>(selectedRows);
 357                 }

 358                 selectedRows.clear();

 359                 addSelectedRows(rendererResult, 0, Math.min(y1, y2), Math.max(y1, y2));
 360                 selectionStart = xStart;
 361                 selectionEnd = xEnd;
 362                 return (oldRows == null) || !oldRows.equals(selectedRows);
 363         }
 364 
 365         public boolean clearSelection() {
 366                 if ((selectionStart == null) && (selectionEnd == null) && selectedRows.isEmpty()) {
 367                         return false;
 368                 }
 369                 selectedRows.clear();
 370                 selectionStart = selectionEnd = null;
 371                 return true;
 372         }
 373 
 374         private boolean addSelectedRows(IRenderedRow row, int yRowStart, int ySelectionStart, int ySelectionEnd) {
 375                 List<IRenderedRow> subdivision = row.getNestedRows();
 376                 if (subdivision.isEmpty()) {
 377                         return addPayload(row);
 378                 } else {
 379                         boolean nestedHasPayload = false;
 380                         for (IRenderedRow nestedRow : row.getNestedRows()) {
 381                                 int yRowEnd = yRowStart + nestedRow.getHeight();
 382                                 if (yRowStart > ySelectionEnd) {
 383                                         break;
 384                                 } else if (yRowEnd > ySelectionStart) {
 385                                         nestedHasPayload |= addSelectedRows(nestedRow, yRowStart, ySelectionStart, ySelectionEnd);
 386                                 }
 387                                 yRowStart = yRowEnd;
 388                         }
 389                         return nestedHasPayload || addPayload(row);
 390                 }
 391         }
 392 
 393         private boolean addPayload(IRenderedRow row) {
 394                 Object payload = row.getPayload();
 395                 if (payload != null) {



 396                         selectedRows.add(payload);

 397                         return true;
 398                 }
 399                 return false;
 400         }
 401 
 402         private void renderSelection(Graphics2D context, SubdividedQuantityRange xRange, int height) {
 403                 int selFrom = 0;
 404                 int selTo = axisWidth;
 405                 if (selectionStart != null && selectionEnd != null) {
 406                         selFrom = (int) xRange.getPixel(selectionStart);
 407                         // Removed "+ 1" for now to make the selection symmetrical with respect to chart highlights.
 408                         selTo = (int) xRange.getPixel(selectionEnd);
 409                 }
 410                 // FIXME: Would like to show selection by graying out the other parts, can we do that?
 411 //              if (selWidth > 0) {
 412 //                      context.setColor(Color.WHITE);
 413 //                      context.setXORMode(Color.BLACK);
 414 //                      Stroke oldStroke = context.getStroke();
 415 //                      context.setStroke(SELECTION_STROKE);
 416 //                      context.drawRect(selFrom, 0, selWidth, height);
 417 //                      context.setStroke(oldStroke);
 418 //                      context.setPaintMode();
 419 //              }
 420                 if (selFrom > 0) {
 421                         dimRect(context, 0, selFrom, height);
 422                         context.setColor(Color.BLACK);
 423                         context.drawLine(selFrom, 0, selFrom, height);
 424                 }
 425                 if (selTo < axisWidth) {
 426                         dimRect(context, selTo, axisWidth - selTo, height);
 427                         context.setColor(Color.BLACK);
 428                         context.drawLine(selTo, 0, selTo, height);
 429                 }
 430         }
 431 
 432         private static void dimRect(Graphics2D context, int from, int width, int height) {
 433                 context.setColor(SELECTION_COLOR);
 434                 context.fillRect(from, 0, width, height);
 435         }
 436 
 437         /**
 438          * Let the {@code visitor} visit the chart elements in the vicinity of {@code x} and {@code y}.
 439          * The visitation should adhere to a basic front to back ordering, so that elements which
 440          * <em>conceptually</em> are at the front should be visited first. Note that this has no direct
 441          * link to the drawing order. Also, this doesn't dictate any particular order between elements
 442          * that conceptually are at the same level. (Good practice is to visit elements from different
 443          * sub charts in a consistent order. If the sub charts have some kind of fixed ordering, such as
 444          * stacked line charts, this order from top to bottom seems most appropriate.)
 445          *
 446          * @param visitor
 447          * @param x
 448          * @param y
 449          */
 450         public void infoAt(IChartInfoVisitor visitor, int x, int y) {
 451                 if (rendererResult == null) {
 452                         return;
 453                 }
 454                 final int height = rendererResult.getHeight();




  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.Graphics2D;
  37 import java.awt.Point;
  38 import java.awt.geom.AffineTransform;
  39 import java.awt.geom.Point2D;
  40 import java.util.ArrayList;
  41 import java.util.HashSet;
  42 import java.util.List;
  43 import java.util.Set;
  44 import java.util.Stack;
  45 import java.util.function.Consumer;
  46 
  47 import org.openjdk.jmc.common.IDisplayable;
  48 import org.openjdk.jmc.common.unit.IQuantity;
  49 import org.openjdk.jmc.common.unit.IRange;
  50 import org.openjdk.jmc.common.unit.QuantitiesToolkit;
  51 import org.openjdk.jmc.common.unit.QuantityRange;
  52 import org.openjdk.jmc.common.unit.UnitLookup;
  53 import org.openjdk.jmc.ui.charts.IChartInfoVisitor.ITick;
  54 import org.openjdk.jmc.ui.misc.ChartDisplayControlBar;
  55 import org.openjdk.jmc.ui.misc.TimelineCanvas;
  56 import org.openjdk.jmc.ui.misc.PatternFly.Palette;
  57 
  58 public class XYChart {
  59         private static final String ELLIPSIS = "..."; //$NON-NLS-1$
  60         private static final Color SELECTION_COLOR = new Color(255, 255, 255, 220);
  61         private static final Color RANGE_INDICATION_COLOR = new Color(255, 60, 20);
  62         private static final int RANGE_INDICATOR_HEIGHT = 7;

  63         private final IQuantity start;
  64         private final IQuantity end;
  65         private IQuantity rangeDuration;
  66         private IXDataRenderer rendererRoot;
  67         private IRenderedRow rendererResult;
  68         private final int xOffset;
  69         private int yOffset = 35;
  70         private final int bucketWidth;
  71         // FIXME: Use bucketWidth * ticksPerBucket instead of hardcoded value?
  72 //      private final int ticksPerBucket = 4;
  73 
  74         private IQuantity currentStart;
  75         private IQuantity currentEnd;
  76 
  77         private final Set<Object> selectedRows = new HashSet<>();
  78         private int axisWidth;
  79         private int rowColorCounter;
  80         private IQuantity selectionStart;
  81         private IQuantity selectionEnd;
  82         private SubdividedQuantityRange xBucketRange;
  83         private SubdividedQuantityRange xTickRange;
  84 
  85         // JFR Threads Page
  86         private static final double ZOOM_PAN_FACTOR = 0.05;
  87         private static final int ZOOM_PAN_MODIFIER = 2;
  88         private double zoomPanPower = ZOOM_PAN_FACTOR / ZOOM_PAN_MODIFIER;
  89         private double currentZoom;
  90         private int zoomSteps;
  91         private ChartDisplayControlBar displayBar;
  92         private ChartFilterControlBar filterBar;
  93         private Stack<Integer> modifiedSteps;
  94         private TimelineCanvas timelineCanvas;
  95 
  96         public XYChart(IRange<IQuantity> range, IXDataRenderer rendererRoot) {
  97                 this(range.getStart(), range.getEnd(), rendererRoot);
  98         }
  99 
 100         public XYChart(IRange<IQuantity> range, IXDataRenderer rendererRoot, int xOffset) {
 101                 this(range.getStart(), range.getEnd(), rendererRoot, xOffset);
 102         }
 103 
 104         // JFR Threads Page
 105         public XYChart(IRange<IQuantity> range, IXDataRenderer rendererRoot, int xOffset, Integer yOffset, TimelineCanvas timelineCanvas, ChartFilterControlBar filterBar, ChartDisplayControlBar displayBar) {
 106                 this(range.getStart(), range.getEnd(), rendererRoot, xOffset);
 107                 this.yOffset = yOffset;
 108                 this.timelineCanvas = timelineCanvas;
 109                 this.filterBar = filterBar;
 110                 this.displayBar = displayBar;
 111                 this.rangeDuration = range.getExtent();
 112                 this.currentZoom = 100;
 113                 this.isZoomCalculated = false;
 114         }
 115         
 116         public XYChart(IRange<IQuantity> range, IXDataRenderer rendererRoot, int xOffset, int bucketWidth) {
 117                 this(range.getStart(), range.getEnd(), rendererRoot, xOffset, bucketWidth);
 118         }
 119 
 120         public XYChart(IQuantity start, IQuantity end, IXDataRenderer rendererRoot) {
 121                 this(start, end, rendererRoot, 60);
 122         }
 123 
 124         public XYChart(IQuantity start, IQuantity end, IXDataRenderer rendererRoot, int xOffset) {
 125                 this(start, end, rendererRoot, xOffset, 25);
 126         }
 127 
 128         public XYChart(IQuantity start, IQuantity end, IXDataRenderer rendererRoot, int xOffset, int bucketWidth) {
 129                 this.rendererRoot = rendererRoot;
 130                 // Start value must always be strictly less than end
 131                 assert (start.compareTo(end) < 0);
 132                 this.currentStart = start;
 133                 this.start = start;
 134                 this.currentEnd = end;
 135                 this.end = end;
 136                 this.xOffset = xOffset;
 137                 this.bucketWidth = bucketWidth;
 138         }
 139         
 140         public void setRendererRoot(IXDataRenderer rendererRoot) {
 141                 clearSelection();
 142                 this.rendererRoot = rendererRoot;
 143         }
 144 
 145         public IXDataRenderer getRendererRoot() {
 146                 return rendererRoot;
 147         }
 148 
 149         public Object[] getSelectedRows() {
 150                 return selectedRows.toArray(new Object[selectedRows.size()]);
 151         }
 152 
 153         public IQuantity getSelectionStart() {
 154                 return selectionStart;
 155         }
 156 
 157         public IQuantity getSelectionEnd() {
 158                 return selectionEnd;
 159         }
 160 
 161         public IRange<IQuantity> getSelectionRange() {
 162                 return (selectionStart != null) && (selectionEnd != null)
 163                                 ? QuantityRange.createWithEnd(selectionStart, selectionEnd) : null;
 164         }
 165 
 166         public void renderChart(Graphics2D context, int width, int height) {
 167                 if (width > xOffset && height > yOffset) {
 168                         axisWidth = width - xOffset;
 169                         // FIXME: xBucketRange and xTickRange should be more related, so that each tick is typically an integer number of buckets (or possibly 2.5 buckets).
 170                         xBucketRange = new SubdividedQuantityRange(currentStart, currentEnd, axisWidth, bucketWidth);
 171                         // FIXME: Use bucketWidth * ticksPerBucket instead of hardcoded value?
 172                         xTickRange = new SubdividedQuantityRange(currentStart, currentEnd, axisWidth, 100);
 173                         AffineTransform oldTransform = context.getTransform();
 174                         context.translate(xOffset, 0);
 175                         doRenderChart(context, height - yOffset);
 176                         context.setTransform(oldTransform);
 177                 }
 178         }
 179 
 180         public void renderTextCanvasText(Graphics2D context, int width) {
 181                 axisWidth = width;
 182                 AffineTransform oldTransform = context.getTransform();
 183                 doRenderTextCanvasText(context);
 184                 context.setTransform(oldTransform);
 185         }
 186 
 187         public void renderText(Graphics2D context, int width, int height) {
 188                 if (width > xOffset && height > yOffset) {
 189                         axisWidth = xOffset;
 190                         AffineTransform oldTransform = context.getTransform();
 191                         doRenderText(context);
 192                         context.setTransform(oldTransform);
 193                         axisWidth = width - xOffset;
 194                 }
 195         }
 196 
 197         private void renderRangeIndication(Graphics2D context, int rangeIndicatorY) {
 198                 // FIXME: Extract the needed functionality from SubdividedQuantityRange
 199                 SubdividedQuantityRange fullRangeAxis = new SubdividedQuantityRange(start, end, axisWidth, 25);
 200                 int x1 = (int) fullRangeAxis.getPixel(currentStart);
 201                 int x2 = (int) Math.ceil(fullRangeAxis.getPixel(currentEnd));
 202                 
 203                 if (timelineCanvas != null) {
 204                         timelineCanvas.renderRangeIndicator(x1, x2);
 205                 } else {
 206                         context.setPaint(RANGE_INDICATION_COLOR);
 207                         context.fillRect(x1, rangeIndicatorY, x2 - x1, RANGE_INDICATOR_HEIGHT);
 208                         context.setPaint(Color.DARK_GRAY);
 209                         context.drawRect(0, rangeIndicatorY, axisWidth - 1, RANGE_INDICATOR_HEIGHT);
 210                 }
 211         }
 212 
 213         private void doRenderChart(Graphics2D context, int axisHeight) {
 214                 rowColorCounter = 0;
 215                 context.setPaint(Color.LIGHT_GRAY);
 216                 AWTChartToolkit.drawGrid(context, xTickRange, axisHeight, false);
 217                 // Attempt to make graphs so low they cover the axis show by drawing the full axis first ...
 218                 context.setPaint(Color.BLACK);
 219                 if (timelineCanvas != null) {
 220                         timelineCanvas.setXTickRange(xTickRange);
 221                 } else {
 222                         AWTChartToolkit.drawAxis(context, xTickRange, axisHeight - 1, false, 1 - xOffset, false);
 223                 }
 224                 // ... then the graph ...
 225                 rendererResult = rendererRoot.render(context, xBucketRange, axisHeight);
 226                 AffineTransform oldTransform = context.getTransform();
 227 
 228                 context.setTransform(oldTransform);
 229                 if (!selectedRows.isEmpty()) {
 230                         renderSelectionChart(context, rendererResult);
 231                         context.setTransform(oldTransform);
 232                 }
 233                 // .. and finally a semitransparent axis line again.
 234                 context.setPaint(new Color(0, 0, 0, 64));
 235                 context.drawLine(0, axisHeight - 1, axisWidth - 1, axisHeight - 1);
 236                 renderRangeIndication(context, axisHeight + 25);
 237         }
 238 
 239         private void doRenderText(Graphics2D context) {
 240                 AffineTransform oldTransform = context.getTransform();
 241                 rowColorCounter = -1;
 242                 renderText(context, rendererResult);
 243                 context.setTransform(oldTransform);
 244         }
 245 
 246         private void doRenderTextCanvasText(Graphics2D context) {
 247                 AffineTransform oldTransform = context.getTransform();
 248                 rowColorCounter = 0;
 249                 renderText(context, rendererResult);
 250                 context.setTransform(oldTransform);
 251                 if (!selectedRows.isEmpty()) {
 252                         renderSelectionText(context, rendererResult);
 253                         context.setTransform(oldTransform);
 254                 }
 255         }
 256 
 257         private void renderSelectionText(Graphics2D context, IRenderedRow row) {
 258                 if (selectedRows.contains(row.getPayload())) {
 259                         if (row.getHeight() != rendererResult.getHeight()) {
 260                                 Color highlight = new Color(0, 206, 209, 20);
 261                                 context.setColor(highlight);
 262                                 context.fillRect(0, 0, axisWidth, row.getHeight());
 263                         } else {
 264                                 selectedRows.clear();
 265                         }
 266                 } else {
 267                         List<IRenderedRow> subdivision = row.getNestedRows();
 268                         if (subdivision.isEmpty()) {
 269                                 dimRect(context, 0, axisWidth, row.getHeight());
 270                         } else {
 271                                 for (IRenderedRow nestedRow : row.getNestedRows()) {
 272                                         renderSelectionText(context, nestedRow);
 273                                 }
 274                                 return;
 275                         }
 276                 }
 277                 context.translate(0, row.getHeight());
 278         }
 279 
 280         private void renderSelectionChart(Graphics2D context, IRenderedRow row) {
 281                 if (selectedRows.contains(row.getPayload())) {
 282                         renderSelection(context, xBucketRange, row.getHeight());
 283                 } else {
 284                         List<IRenderedRow> subdivision = row.getNestedRows();
 285                         if (subdivision.isEmpty()) {
 286                                 dimRect(context, 0, axisWidth, row.getHeight());
 287                         } else {
 288                                 for (IRenderedRow nestedRow : row.getNestedRows()) {
 289                                         renderSelectionChart(context, nestedRow);
 290                                 }
 291                                 return;
 292                         }
 293                 }
 294                 context.translate(0, row.getHeight());
 295         }
 296 
 297         // Paint the background of every-other row in a slightly different shade
 298         // to better differentiate the thread lanes from one another
 299         private void paintRowBackground(Graphics2D context, int height) {
 300                 if (rowColorCounter >= 0) {
 301                         if (rowColorCounter % 2 == 0) {
 302                                 context.setColor(Palette.PF_BLACK_100.getAWTColor());
 303                         } else {
 304                                 context.setColor(Palette.PF_BLACK_200.getAWTColor());
 305                         }
 306                         context.fillRect(0, 0, axisWidth, height);
 307                         rowColorCounter++;
 308                 }
 309         }
 310 
 311         private void renderText(Graphics2D context, IRenderedRow row) {
 312                 String text = row.getName();
 313                 int height = row.getHeight();
 314                 if (height >= context.getFontMetrics().getHeight()) {
 315                         if (text != null) {
 316                                 paintRowBackground(context, row.getHeight());
 317                                 context.setColor(Color.BLACK);
 318                                 context.drawLine(0, height - 1, axisWidth -15, height - 1);
 319                                 int y = ((height - context.getFontMetrics().getHeight()) / 2) + context.getFontMetrics().getAscent();






 320                                 int charsWidth = context.getFontMetrics().charsWidth(text.toCharArray(), 0, text.length());
 321                                 if (xOffset > 0 && charsWidth > xOffset) {
 322                                         float fitRatio = ((float) xOffset) / (charsWidth
 323                                                         + context.getFontMetrics().charsWidth(ELLIPSIS.toCharArray(), 0, ELLIPSIS.length()));
 324                                         text = text.substring(0, ((int) (text.length() * fitRatio)) - 1) + ELLIPSIS;
 325                                 }
 326                                 context.drawString(text, 2, y);
 327                         } else {
 328                                 List<IRenderedRow> subdivision = row.getNestedRows();
 329                                 if (!subdivision.isEmpty()) {
 330                                         for (IRenderedRow nestedRow : row.getNestedRows()) {
 331                                                 renderText(context, nestedRow);
 332                                         }
 333                                         return;
 334                                 }
 335                         }
 336                 }
 337                 context.translate(0, height);
 338         }
 339 
 340         /**
 341          * Pan the view.
 342          *
 343          * @param rightPercent
 344          * @return true if the bounds changed. That is, if a redraw is required.
 345          */
 346         public boolean pan(int rightPercent) {
 347                 if (rangeDuration != null) {
 348                         return panRange(Integer.signum(rightPercent));
 349                 }
 350                 if (xBucketRange != null) {
 351                         IQuantity oldStart = currentStart;
 352                         IQuantity oldEnd = currentEnd;
 353                         if (rightPercent > 0) {
 354                                 currentEnd = QuantitiesToolkit
 355                                                 .min(xBucketRange.getQuantityAtPixel(axisWidth + axisWidth * rightPercent / 100), end);
 356                                 currentStart = QuantitiesToolkit
 357                                                 .max(xBucketRange.getQuantityAtPixel(xBucketRange.getPixel(currentEnd) - axisWidth), start);
 358                         } else if (rightPercent < 0) {
 359                                 currentStart = QuantitiesToolkit.max(xBucketRange.getQuantityAtPixel(axisWidth * rightPercent / 100),
 360                                                 start);
 361                                 currentEnd = QuantitiesToolkit
 362                                                 .min(xBucketRange.getQuantityAtPixel(xBucketRange.getPixel(currentStart) + axisWidth), end);
 363                         }
 364                         return (currentStart.compareTo(oldStart) != 0) || (currentEnd.compareTo(oldEnd) != 0);
 365                 }
 366                 // Return true since a redraw forces creation of xBucketRange.
 367                 return true;
 368         }
 369 
 370         /**
 371          * Pan the view at a rate relative the current zoom level.
 372          * @param panDirection -1 to pan left, 1 to pan right
 373          * @return true if the chart needs to be redrawn
 374          */
 375         public boolean panRange(int panDirection) {
 376                 if (zoomSteps == 0 || panDirection == 0 ||
 377                                 (currentStart.compareTo(start) == 0 && panDirection == -1) ||
 378                                 (currentEnd.compareTo(end) == 0 && panDirection == 1)) {
 379                         return false;
 380                 }
 381 
 382                 IQuantity panDiff = rangeDuration.multiply(panDirection * zoomPanPower);
 383                 IQuantity newStart = currentStart.in(UnitLookup.EPOCH_NS).add(panDiff);
 384                 IQuantity newEnd = currentEnd.in(UnitLookup.EPOCH_NS).add(panDiff);
 385 
 386                 // if panning would flow over the recording range start or end time,
 387                 // calculate the difference and add it so the other side.
 388                 if (newStart.compareTo(start) < 0) {
 389                         IQuantity diff = start.subtract(newStart);
 390                         newStart = start;
 391                         newEnd = newEnd.add(diff);
 392                 } else if (newEnd.compareTo(end) > 0) {
 393                         IQuantity diff = newEnd.subtract(end);
 394                         newStart = newStart.add(diff);
 395                         newEnd = end;
 396                 }
 397                 currentStart = newStart;
 398                 currentEnd = newEnd;
 399                 filterBar.setStartTime(currentStart);
 400                 filterBar.setEndTime(currentEnd);
 401                 isZoomCalculated = true;
 402                 return true;
 403         }
 404 
 405         /**
 406          * Zoom the view.
 407          *
 408          * @param zoomInSteps
 409          * @return true if the bounds changed. That is, if a redraw is required.
 410          */
 411         public boolean zoom(int zoomInSteps) {
 412                 if (rangeDuration != null) {
 413                         return zoomRange(zoomInSteps);
 414                 }
 415                 return zoomXAxis(axisWidth / 2, zoomInSteps);
 416         }
 417 
 418         /**
 419          * Zoom the view.
 420          *
 421          * @param x
 422          * @param zoomInSteps
 423          * @return true if the bounds changed. That is, if a redraw is required.
 424          */
 425         public boolean zoom(int x, int zoomInSteps) {
 426                 return zoomXAxis(x - xOffset, zoomInSteps);
 427         }
 428 
 429         // Default zoom mechanics
 430         private boolean zoomXAxis(int x, int zoomInSteps) {
 431                 if (xBucketRange == null) {
 432                         // Return true since a redraw forces creation of xBucketRange.
 433                         return true;
 434                 }
 435                 if ((x > 0) && (x < axisWidth)) {
 436                         IQuantity oldStart = currentStart;
 437                         IQuantity oldEnd = currentEnd;
 438                         // Absolute value of zoomFactor must be less than 1. Currently it ranges between -0.5 and 0.5.
 439                         double zoomFactor = Math.atan(zoomInSteps) / Math.PI;
 440                         int newStart = (int) (zoomFactor * x);
 441                         int newEnd = (int) (axisWidth * (1 - zoomFactor)) + newStart;
 442                         SubdividedQuantityRange xAxis = new SubdividedQuantityRange(currentStart, currentEnd, axisWidth, 1);
 443                         setVisibleRange(xAxis.getQuantityAtPixel(newStart), xAxis.getQuantityAtPixel(newEnd));
 444                         return (currentStart.compareTo(oldStart) != 0) || (currentEnd.compareTo(oldEnd) != 0);
 445                 }
 446                 return false;
 447         }
 448 
 449         /**
 450          * Zoom based on a percentage of the recording range
 451          * @param zoomInSteps
 452          * @return true if a redraw is required as a result of a successful zoom
 453          */
 454         public boolean zoomRange(int zoomInSteps) {
 455                 if (zoomInSteps > 0) {
 456                         zoomIn();
 457                 } else {
 458                         if (zoomSteps == 0) {
 459                                 return false;
 460                         }
 461                         zoomOut();
 462                 }
 463                 // set displayBar text
 464                 displayBar.setZoomPercentageText(currentZoom);
 465                 return true;
 466         }
 467 
 468         /** 
 469          * Zoom into the chart at a rate of 5% of the overall recording range at each step.
 470          * If the chart is zoomed in far enough such that one more step at 5% is not possible,
 471          * the zoom power is halved and the zoom will proceed.
 472          * <br>
 473          * Every time the zoom power is halved, the instigating step value is pushed onto the
 474          * modifiedSteps stack. This stack is consulted on zoom out events in order to ensure
 475          * the chart zooms out the same way it was zoomed in.
 476          */
 477         private void zoomIn() {
 478                 IQuantity zoomDiff = rangeDuration.multiply(zoomPanPower);
 479                 IQuantity newStart = currentStart.in(UnitLookup.EPOCH_NS).add(zoomDiff);
 480                 IQuantity newEnd = currentEnd.in(UnitLookup.EPOCH_NS).subtract(zoomDiff);
 481                 if (newStart.compareTo(newEnd) >= 0) { // adjust the zoom factor
 482                         if (modifiedSteps == null) {
 483                                 modifiedSteps = new Stack<Integer>();
 484                         }
 485                         modifiedSteps.push(zoomSteps);
 486                         zoomPanPower = zoomPanPower / ZOOM_PAN_MODIFIER;
 487                         zoomDiff = rangeDuration.multiply(zoomPanPower);
 488                         newStart = currentStart.in(UnitLookup.EPOCH_NS).add(zoomDiff);
 489                         newEnd = currentEnd.in(UnitLookup.EPOCH_NS).subtract(zoomDiff);
 490                 }
 491                 currentZoom = currentZoom + (zoomPanPower * ZOOM_PAN_MODIFIER * 100);
 492                 isZoomCalculated = true;
 493                 setVisibleRange(newStart, newEnd);
 494                 zoomSteps++;
 495         }
 496 
 497         /**
 498          * Zoom out of the chart at a rate equal to the how the chart was zoomed in.
 499          */
 500         private void zoomOut() {
 501                 if (modifiedSteps != null && modifiedSteps.size() > 0 && modifiedSteps.peek() == zoomSteps) {
 502                         modifiedSteps.pop();
 503                         zoomPanPower = zoomPanPower * ZOOM_PAN_MODIFIER;
 504                 }
 505                 IQuantity zoomDiff = rangeDuration.multiply(zoomPanPower);
 506                 IQuantity newStart = currentStart.in(UnitLookup.EPOCH_NS).subtract(zoomDiff);
 507                 IQuantity newEnd = currentEnd.in(UnitLookup.EPOCH_NS).add(zoomDiff);
 508 
 509                 // if zooming out would flow over the recording range start or end time,
 510                 // calculate the difference and add it to the other side.
 511                 if (newStart.compareTo(start) < 0) {
 512                         IQuantity diff = start.subtract(newStart);
 513                         newStart = start;
 514                         newEnd = newEnd.add(diff);
 515                 } else if (newEnd.compareTo(end) > 0) {
 516                         IQuantity diff = newEnd.subtract(end);
 517                         newStart = newStart.subtract(diff);
 518                         newEnd = end;
 519                 }
 520                 currentZoom = currentZoom - (zoomPanPower * ZOOM_PAN_MODIFIER * 100);
 521                 if (currentZoom < 100) {
 522                         currentZoom = 100;
 523                 }
 524                 isZoomCalculated = true;
 525                 setVisibleRange(newStart, newEnd);
 526                 zoomSteps--;
 527         }
 528 
 529         // need to check from ChartAndPopupTableUI if not using the OG start/end position,
 530         // will have to calculate the new zoom level
 531         public void resetZoomFactor() {
 532                 zoomSteps = 0;
 533                 zoomPanPower = ZOOM_PAN_FACTOR / ZOOM_PAN_MODIFIER;
 534                 currentZoom = 100;
 535                 displayBar.setZoomPercentageText(currentZoom);
 536                 modifiedSteps = new Stack<Integer>();
 537         }
 538 
 539         /**
 540          *  Reset the visible range to be the recording range, and reset the zoom-related objects
 541          */
 542         public void resetTimeline() {
 543                 resetZoomFactor();
 544                 setVisibleRange(start, end);
 545         }
 546 
 547         private void selectionZoom(IQuantity newStart, IQuantity newEnd) {
 548                 double percentage = calculateZoom(newStart, newEnd);
 549                 zoomSteps = calculateZoomSteps(percentage);
 550                 currentZoom = 100 + (percentage * 100);
 551                 displayBar.setScaleValue(zoomSteps);
 552                 displayBar.setZoomPercentageText(currentZoom);
 553         }
 554         
 555         /**
 556          *  When a drag-select zoom occurs, use the new range value to determine how many steps have been taken,
 557          *  and adjust zoomSteps and zoomPower accordingly
 558          */
 559         private double calculateZoom(IQuantity newStart, IQuantity newEnd) {
 560                 // calculate the new visible range, and it's percentage of the total range
 561                 IQuantity newRange = newEnd.in(UnitLookup.EPOCH_NS).subtract(newStart.in(UnitLookup.EPOCH_NS));
 562                 return 1 - (newRange.longValue() / (double) rangeDuration.in(UnitLookup.NANOSECOND).longValue());       
 563         }
 564 
 565         /**
 566          * Calculate the number of steps required to achieve the passed zoom percentage
 567          */
 568         private int calculateZoomSteps(double percentage) {
 569                 double tempPercent = 0;
 570                 int steps = 0;
 571                 do {
 572                         tempPercent = tempPercent + ZOOM_PAN_FACTOR;
 573                         steps++;
 574                 } while (tempPercent <= percentage);
 575                 return steps;
 576         }
 577 
 578         private boolean isZoomCalculated;
 579         private boolean isZoomPanDrag;
 580 
 581         public void setIsZoomPanDrag(boolean isZoomPanDrag) {
 582                 this.isZoomPanDrag = isZoomPanDrag;
 583         }
 584 
 585         private boolean getIsZoomPanDrag() {
 586                 return isZoomPanDrag;
 587         }
 588 
 589         public void setVisibleRange(IQuantity rangeStart, IQuantity rangeEnd) {
 590                 if (rangeDuration != null && !isZoomCalculated && rangeStart != start && !getIsZoomPanDrag()) {
 591                         selectionZoom(rangeStart, rangeEnd);
 592                 }
 593                 isZoomCalculated = false;
 594                 rangeStart = QuantitiesToolkit.max(rangeStart, start);
 595                 rangeEnd = QuantitiesToolkit.min(rangeEnd, end);
 596                 if (rangeStart.compareTo(rangeEnd) < 0) {
 597                         SubdividedQuantityRange testRange = new SubdividedQuantityRange(rangeStart, rangeEnd, 10000, 1);
 598                         if (testRange.getQuantityAtPixel(0).compareTo(testRange.getQuantityAtPixel(1)) < 0) {
 599                                 currentStart = rangeStart;
 600                                 currentEnd = rangeEnd;
 601                         } else {
 602                                 // Ensures that zoom out is always allowed
 603                                 currentStart = QuantitiesToolkit.min(rangeStart, currentStart);
 604                                 currentEnd = QuantitiesToolkit.max(rangeEnd, currentEnd);
 605                         }
 606                         if (filterBar != null) {
 607                                 filterBar.setStartTime(currentStart);
 608                                 filterBar.setEndTime(currentEnd);
 609                         }
 610                         rangeListeners.stream().forEach(l -> l.accept(getVisibleRange()));
 611                 }
 612         }
 613 
 614         private List<Consumer<IRange<IQuantity>>> rangeListeners = new ArrayList<>();
 615 
 616         public void addVisibleRangeListener(Consumer<IRange<IQuantity>> rangeListener) {
 617                 rangeListeners.add(rangeListener);
 618         }
 619 
 620         public IRange<IQuantity> getVisibleRange() {
 621                 return (currentStart != null) && (currentEnd != null) ? QuantityRange.createWithEnd(currentStart, currentEnd)
 622                                 : null;
 623         }
 624 
 625         public void clearVisibleRange() {
 626                 currentStart = start;
 627                 currentEnd = end;
 628         }
 629 
 630         public boolean select(int x1, int x2, int y1, int y2, boolean clear) {
 631                 int xStart = Math.min(x1, x2);
 632                 int xEnd = Math.max(x1, x2);
 633 
 634                 if (xBucketRange != null && (xEnd != xStart) && xEnd - xOffset >= 0) {
 635                         return select(xBucketRange.getQuantityAtPixel(Math.max(0, xStart - xOffset)), xBucketRange.getQuantityAtPixel(xEnd - xOffset),
 636                                         y1, y2, clear);
 637                 } else {
 638                         return select(null, null, y1, y2, clear);
 639                 }
 640         }
 641 
 642         public boolean select(IQuantity xStart, IQuantity xEnd, int y1, int y2, boolean clear) {
 643                 if (xStart != null && xStart.compareTo(start) < 0) {
 644                         xStart = start;
 645                 }
 646                 if (xEnd != null && xEnd.compareTo(end) > 0) {
 647                         xEnd = end;
 648                 }
 649                 Set<Object> oldRows = null;
 650                 if (QuantitiesToolkit.same(selectionStart, xStart) && QuantitiesToolkit.same(selectionEnd, xEnd)) {
 651                         oldRows = new HashSet<>(selectedRows);
 652                 }
 653                 if (clear) {
 654                         selectedRows.clear();
 655                 }
 656                 addSelectedRows(rendererResult, 0, Math.min(y1, y2), Math.max(y1, y2));
 657                 selectionStart = xStart;
 658                 selectionEnd = xEnd;
 659                 return (oldRows == null) || !oldRows.equals(selectedRows);
 660         }
 661 
 662         public boolean clearSelection() {
 663                 if ((selectionStart == null) && (selectionEnd == null) && selectedRows.isEmpty()) {
 664                         return false;
 665                 }
 666                 selectedRows.clear();
 667                 selectionStart = selectionEnd = null;
 668                 return true;
 669         }
 670 
 671         private boolean addSelectedRows(IRenderedRow row, int yRowStart, int ySelectionStart, int ySelectionEnd) {
 672                 List<IRenderedRow> subdivision = row.getNestedRows(); // height 1450, has all 32 rows
 673                 if (subdivision.isEmpty()) {
 674                         return addPayload(row);
 675                 } else {
 676                         boolean nestedHasPayload = false;
 677                         for (IRenderedRow nestedRow : row.getNestedRows()) {
 678                                 int yRowEnd = yRowStart + nestedRow.getHeight();
 679                                 if (yRowStart > ySelectionEnd) {
 680                                         break;
 681                                 } else if (yRowEnd > ySelectionStart) {
 682                                         nestedHasPayload |= addSelectedRows(nestedRow, yRowStart, ySelectionStart, ySelectionEnd);
 683                                 }
 684                                 yRowStart = yRowEnd;
 685                         }
 686                         return nestedHasPayload || addPayload(row);
 687                 }
 688         }
 689 
 690         private boolean addPayload(IRenderedRow row) {
 691                 Object payload = row.getPayload();
 692                 if (payload != null) {
 693                         if (selectedRows.contains(payload)) { // ctrl+click deselection
 694                                 selectedRows.remove(payload);
 695                         } else {
 696                                 selectedRows.add(payload);
 697                         }
 698                         return true;
 699                 }
 700                 return false;
 701         }
 702 
 703         private void renderSelection(Graphics2D context, SubdividedQuantityRange xRange, int height) {
 704                 int selFrom = 0;
 705                 int selTo = axisWidth;
 706                 if (selectionStart != null && selectionEnd != null) {
 707                         selFrom = (int) xRange.getPixel(selectionStart);
 708                         // Removed "+ 1" for now to make the selection symmetrical with respect to chart highlights.
 709                         selTo = (int) xRange.getPixel(selectionEnd);
 710                 }
 711                 // FIXME: Would like to show selection by graying out the other parts, can we do that?
 712 //              if (selWidth > 0) {
 713 //                      context.setColor(Color.WHITE);
 714 //                      context.setXORMode(Color.BLACK);
 715 //                      Stroke oldStroke = context.getStroke();
 716 //                      context.setStroke(SELECTION_STROKE);
 717 //                      context.drawRect(selFrom, 0, selWidth, height);
 718 //                      context.setStroke(oldStroke);
 719 //                      context.setPaintMode();
 720 //              }
 721                 if (selFrom > 0) {
 722                         dimRect(context, 0, selFrom, height);
 723                         context.setColor(Color.BLACK);
 724                         context.drawLine(selFrom, 0, selFrom, height);
 725                 }
 726                 if (selTo < axisWidth) {
 727                         dimRect(context, selTo, axisWidth - selTo, height);
 728                         context.setColor(Color.BLACK);
 729                         context.drawLine(selTo, 0, selTo, height);
 730                 }
 731         }
 732 
 733         private static void dimRect(Graphics2D context, int from, int width, int height) {
 734                 context.setColor(SELECTION_COLOR);
 735                 context.fillRect(from , 0, width, height);
 736         }
 737 
 738         /**
 739          * Let the {@code visitor} visit the chart elements in the vicinity of {@code x} and {@code y}.
 740          * The visitation should adhere to a basic front to back ordering, so that elements which
 741          * <em>conceptually</em> are at the front should be visited first. Note that this has no direct
 742          * link to the drawing order. Also, this doesn't dictate any particular order between elements
 743          * that conceptually are at the same level. (Good practice is to visit elements from different
 744          * sub charts in a consistent order. If the sub charts have some kind of fixed ordering, such as
 745          * stacked line charts, this order from top to bottom seems most appropriate.)
 746          *
 747          * @param visitor
 748          * @param x
 749          * @param y
 750          */
 751         public void infoAt(IChartInfoVisitor visitor, int x, int y) {
 752                 if (rendererResult == null) {
 753                         return;
 754                 }
 755                 final int height = rendererResult.getHeight();


< prev index next >