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(); |