< prev index next >
application/org.openjdk.jmc.ui/src/main/java/org/openjdk/jmc/ui/charts/XYChart.java
Print this page
*** 39,90 ****
import java.awt.geom.Point2D;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Consumer;
import org.openjdk.jmc.common.IDisplayable;
import org.openjdk.jmc.common.unit.IQuantity;
import org.openjdk.jmc.common.unit.IRange;
import org.openjdk.jmc.common.unit.QuantitiesToolkit;
import org.openjdk.jmc.common.unit.QuantityRange;
import org.openjdk.jmc.ui.charts.IChartInfoVisitor.ITick;
public class XYChart {
private static final String ELLIPSIS = "..."; //$NON-NLS-1$
private static final Color SELECTION_COLOR = new Color(255, 255, 255, 220);
private static final Color RANGE_INDICATION_COLOR = new Color(255, 60, 20);
! private static final int Y_OFFSET = 35;
! private static final int RANGE_INDICATOR_HEIGHT = 4;
private final IQuantity start;
private final IQuantity end;
private IXDataRenderer rendererRoot;
private IRenderedRow rendererResult;
private final int xOffset;
private final int bucketWidth;
// FIXME: Use bucketWidth * ticksPerBucket instead of hardcoded value?
// private final int ticksPerBucket = 4;
private IQuantity currentStart;
private IQuantity currentEnd;
private final Set<Object> selectedRows = new HashSet<>();
private IQuantity selectionStart;
private IQuantity selectionEnd;
private SubdividedQuantityRange xBucketRange;
private SubdividedQuantityRange xTickRange;
! private int axisWidth;
public XYChart(IRange<IQuantity> range, IXDataRenderer rendererRoot) {
this(range.getStart(), range.getEnd(), rendererRoot);
}
public XYChart(IRange<IQuantity> range, IXDataRenderer rendererRoot, int xOffset) {
this(range.getStart(), range.getEnd(), rendererRoot, xOffset);
}
public XYChart(IRange<IQuantity> range, IXDataRenderer rendererRoot, int xOffset, int bucketWidth) {
this(range.getStart(), range.getEnd(), rendererRoot, xOffset, bucketWidth);
}
public XYChart(IQuantity start, IQuantity end, IXDataRenderer rendererRoot) {
--- 39,120 ----
import java.awt.geom.Point2D;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
+ import java.util.Stack;
import java.util.function.Consumer;
import org.openjdk.jmc.common.IDisplayable;
import org.openjdk.jmc.common.unit.IQuantity;
import org.openjdk.jmc.common.unit.IRange;
import org.openjdk.jmc.common.unit.QuantitiesToolkit;
import org.openjdk.jmc.common.unit.QuantityRange;
+ import org.openjdk.jmc.common.unit.UnitLookup;
import org.openjdk.jmc.ui.charts.IChartInfoVisitor.ITick;
+ import org.openjdk.jmc.ui.misc.ChartDisplayControlBar;
+ import org.openjdk.jmc.ui.misc.TimelineCanvas;
+ import org.openjdk.jmc.ui.misc.PatternFly.Palette;
public class XYChart {
private static final String ELLIPSIS = "..."; //$NON-NLS-1$
private static final Color SELECTION_COLOR = new Color(255, 255, 255, 220);
private static final Color RANGE_INDICATION_COLOR = new Color(255, 60, 20);
! private static final int RANGE_INDICATOR_HEIGHT = 7;
private final IQuantity start;
private final IQuantity end;
+ private IQuantity rangeDuration;
private IXDataRenderer rendererRoot;
private IRenderedRow rendererResult;
private final int xOffset;
+ private int yOffset = 35;
private final int bucketWidth;
// FIXME: Use bucketWidth * ticksPerBucket instead of hardcoded value?
// private final int ticksPerBucket = 4;
private IQuantity currentStart;
private IQuantity currentEnd;
private final Set<Object> selectedRows = new HashSet<>();
+ private int axisWidth;
+ private int rowColorCounter;
private IQuantity selectionStart;
private IQuantity selectionEnd;
private SubdividedQuantityRange xBucketRange;
private SubdividedQuantityRange xTickRange;
!
! // JFR Threads Page
! private static final double ZOOM_PAN_FACTOR = 0.05;
! private static final int ZOOM_PAN_MODIFIER = 2;
! private double zoomPanPower = ZOOM_PAN_FACTOR / ZOOM_PAN_MODIFIER;
! private double currentZoom;
! private int zoomSteps;
! private ChartDisplayControlBar displayBar;
! private ChartFilterControlBar filterBar;
! private Stack<Integer> modifiedSteps;
! private TimelineCanvas timelineCanvas;
public XYChart(IRange<IQuantity> range, IXDataRenderer rendererRoot) {
this(range.getStart(), range.getEnd(), rendererRoot);
}
public XYChart(IRange<IQuantity> range, IXDataRenderer rendererRoot, int xOffset) {
this(range.getStart(), range.getEnd(), rendererRoot, xOffset);
}
+ // JFR Threads Page
+ public XYChart(IRange<IQuantity> range, IXDataRenderer rendererRoot, int xOffset, Integer yOffset, TimelineCanvas timelineCanvas, ChartFilterControlBar filterBar, ChartDisplayControlBar displayBar) {
+ this(range.getStart(), range.getEnd(), rendererRoot, xOffset);
+ this.yOffset = yOffset;
+ this.timelineCanvas = timelineCanvas;
+ this.filterBar = filterBar;
+ this.displayBar = displayBar;
+ this.rangeDuration = range.getExtent();
+ this.currentZoom = 100;
+ this.isZoomCalculated = false;
+ }
+
public XYChart(IRange<IQuantity> range, IXDataRenderer rendererRoot, int xOffset, int bucketWidth) {
this(range.getStart(), range.getEnd(), rendererRoot, xOffset, bucketWidth);
}
public XYChart(IQuantity start, IQuantity end, IXDataRenderer rendererRoot) {
*** 97,109 ****
public XYChart(IQuantity start, IQuantity end, IXDataRenderer rendererRoot, int xOffset, int bucketWidth) {
this.rendererRoot = rendererRoot;
// Start value must always be strictly less than end
assert (start.compareTo(end) < 0);
! currentStart = start;
this.start = start;
! currentEnd = end;
this.end = end;
this.xOffset = xOffset;
this.bucketWidth = bucketWidth;
}
--- 127,139 ----
public XYChart(IQuantity start, IQuantity end, IXDataRenderer rendererRoot, int xOffset, int bucketWidth) {
this.rendererRoot = rendererRoot;
// Start value must always be strictly less than end
assert (start.compareTo(end) < 0);
! this.currentStart = start;
this.start = start;
! this.currentEnd = end;
this.end = end;
this.xOffset = xOffset;
this.bucketWidth = bucketWidth;
}
*** 131,226 ****
public IRange<IQuantity> getSelectionRange() {
return (selectionStart != null) && (selectionEnd != null)
? QuantityRange.createWithEnd(selectionStart, selectionEnd) : null;
}
! public void render(Graphics2D context, int width, int height) {
! if (width > xOffset && height > Y_OFFSET) {
axisWidth = width - xOffset;
// FIXME: xBucketRange and xTickRange should be more related, so that each tick is typically an integer number of buckets (or possibly 2.5 buckets).
xBucketRange = new SubdividedQuantityRange(currentStart, currentEnd, axisWidth, bucketWidth);
// FIXME: Use bucketWidth * ticksPerBucket instead of hardcoded value?
xTickRange = new SubdividedQuantityRange(currentStart, currentEnd, axisWidth, 100);
AffineTransform oldTransform = context.getTransform();
context.translate(xOffset, 0);
! doRender(context, height - Y_OFFSET);
context.setTransform(oldTransform);
}
}
private void renderRangeIndication(Graphics2D context, int rangeIndicatorY) {
// FIXME: Extract the needed functionality from SubdividedQuantityRange
SubdividedQuantityRange fullRangeAxis = new SubdividedQuantityRange(start, end, axisWidth, 25);
int x1 = (int) fullRangeAxis.getPixel(currentStart);
int x2 = (int) Math.ceil(fullRangeAxis.getPixel(currentEnd));
! if (x1 > 0 || x2 < axisWidth) {
context.setPaint(RANGE_INDICATION_COLOR);
context.fillRect(x1, rangeIndicatorY, x2 - x1, RANGE_INDICATOR_HEIGHT);
context.setPaint(Color.DARK_GRAY);
context.drawRect(0, rangeIndicatorY, axisWidth - 1, RANGE_INDICATOR_HEIGHT);
}
}
! private void doRender(Graphics2D context, int axisHeight) {
context.setPaint(Color.LIGHT_GRAY);
AWTChartToolkit.drawGrid(context, xTickRange, axisHeight, false);
// Attempt to make graphs so low they cover the axis show by drawing the full axis first ...
context.setPaint(Color.BLACK);
AWTChartToolkit.drawAxis(context, xTickRange, axisHeight - 1, false, 1 - xOffset, false);
// ... then the graph ...
rendererResult = rendererRoot.render(context, xBucketRange, axisHeight);
AffineTransform oldTransform = context.getTransform();
! renderText(context, rendererResult);
context.setTransform(oldTransform);
if (!selectedRows.isEmpty()) {
! renderSelection(context, rendererResult);
context.setTransform(oldTransform);
}
// .. and finally a semitransparent axis line again.
context.setPaint(new Color(0, 0, 0, 64));
context.drawLine(0, axisHeight - 1, axisWidth - 1, axisHeight - 1);
renderRangeIndication(context, axisHeight + 25);
}
! private void renderSelection(Graphics2D context, IRenderedRow row) {
if (selectedRows.contains(row.getPayload())) {
renderSelection(context, xBucketRange, row.getHeight());
} else {
List<IRenderedRow> subdivision = row.getNestedRows();
if (subdivision.isEmpty()) {
dimRect(context, 0, axisWidth, row.getHeight());
} else {
for (IRenderedRow nestedRow : row.getNestedRows()) {
! renderSelection(context, nestedRow);
}
return;
}
}
context.translate(0, row.getHeight());
}
private void renderText(Graphics2D context, IRenderedRow row) {
String text = row.getName();
int height = row.getHeight();
if (height >= context.getFontMetrics().getHeight()) {
if (text != null) {
context.setColor(Color.BLACK);
! int y;
! if (height > 40) {
! context.drawLine(-xOffset, height - 1, -15, height - 1);
! y = height - context.getFontMetrics().getHeight() / 2;
! } else {
! // draw the string in the middle of the row
! y = ((height - context.getFontMetrics().getHeight()) / 2) + context.getFontMetrics().getAscent();
! }
int charsWidth = context.getFontMetrics().charsWidth(text.toCharArray(), 0, text.length());
! if (charsWidth > xOffset) {
float fitRatio = ((float) xOffset) / (charsWidth
+ context.getFontMetrics().charsWidth(ELLIPSIS.toCharArray(), 0, ELLIPSIS.length()));
text = text.substring(0, ((int) (text.length() * fitRatio)) - 1) + ELLIPSIS;
}
! context.drawString(text, -xOffset + 2, y);
} else {
List<IRenderedRow> subdivision = row.getNestedRows();
if (!subdivision.isEmpty()) {
for (IRenderedRow nestedRow : row.getNestedRows()) {
renderText(context, nestedRow);
--- 161,331 ----
public IRange<IQuantity> getSelectionRange() {
return (selectionStart != null) && (selectionEnd != null)
? QuantityRange.createWithEnd(selectionStart, selectionEnd) : null;
}
! public void renderChart(Graphics2D context, int width, int height) {
! if (width > xOffset && height > yOffset) {
axisWidth = width - xOffset;
// FIXME: xBucketRange and xTickRange should be more related, so that each tick is typically an integer number of buckets (or possibly 2.5 buckets).
xBucketRange = new SubdividedQuantityRange(currentStart, currentEnd, axisWidth, bucketWidth);
// FIXME: Use bucketWidth * ticksPerBucket instead of hardcoded value?
xTickRange = new SubdividedQuantityRange(currentStart, currentEnd, axisWidth, 100);
AffineTransform oldTransform = context.getTransform();
context.translate(xOffset, 0);
! doRenderChart(context, height - yOffset);
context.setTransform(oldTransform);
}
}
+ public void renderTextCanvasText(Graphics2D context, int width) {
+ axisWidth = width;
+ AffineTransform oldTransform = context.getTransform();
+ doRenderTextCanvasText(context);
+ context.setTransform(oldTransform);
+ }
+
+ public void renderText(Graphics2D context, int width, int height) {
+ if (width > xOffset && height > yOffset) {
+ axisWidth = xOffset;
+ AffineTransform oldTransform = context.getTransform();
+ doRenderText(context);
+ context.setTransform(oldTransform);
+ axisWidth = width - xOffset;
+ }
+ }
+
private void renderRangeIndication(Graphics2D context, int rangeIndicatorY) {
// FIXME: Extract the needed functionality from SubdividedQuantityRange
SubdividedQuantityRange fullRangeAxis = new SubdividedQuantityRange(start, end, axisWidth, 25);
int x1 = (int) fullRangeAxis.getPixel(currentStart);
int x2 = (int) Math.ceil(fullRangeAxis.getPixel(currentEnd));
!
! if (timelineCanvas != null) {
! timelineCanvas.renderRangeIndicator(x1, x2);
! } else {
context.setPaint(RANGE_INDICATION_COLOR);
context.fillRect(x1, rangeIndicatorY, x2 - x1, RANGE_INDICATOR_HEIGHT);
context.setPaint(Color.DARK_GRAY);
context.drawRect(0, rangeIndicatorY, axisWidth - 1, RANGE_INDICATOR_HEIGHT);
}
}
! private void doRenderChart(Graphics2D context, int axisHeight) {
! rowColorCounter = 0;
context.setPaint(Color.LIGHT_GRAY);
AWTChartToolkit.drawGrid(context, xTickRange, axisHeight, false);
// Attempt to make graphs so low they cover the axis show by drawing the full axis first ...
context.setPaint(Color.BLACK);
+ if (timelineCanvas != null) {
+ timelineCanvas.setXTickRange(xTickRange);
+ } else {
AWTChartToolkit.drawAxis(context, xTickRange, axisHeight - 1, false, 1 - xOffset, false);
+ }
// ... then the graph ...
rendererResult = rendererRoot.render(context, xBucketRange, axisHeight);
AffineTransform oldTransform = context.getTransform();
!
context.setTransform(oldTransform);
if (!selectedRows.isEmpty()) {
! renderSelectionChart(context, rendererResult);
context.setTransform(oldTransform);
}
// .. and finally a semitransparent axis line again.
context.setPaint(new Color(0, 0, 0, 64));
context.drawLine(0, axisHeight - 1, axisWidth - 1, axisHeight - 1);
renderRangeIndication(context, axisHeight + 25);
}
! private void doRenderText(Graphics2D context) {
! AffineTransform oldTransform = context.getTransform();
! rowColorCounter = -1;
! renderText(context, rendererResult);
! context.setTransform(oldTransform);
! }
!
! private void doRenderTextCanvasText(Graphics2D context) {
! AffineTransform oldTransform = context.getTransform();
! rowColorCounter = 0;
! renderText(context, rendererResult);
! context.setTransform(oldTransform);
! if (!selectedRows.isEmpty()) {
! renderSelectionText(context, rendererResult);
! context.setTransform(oldTransform);
! }
! }
!
! private void renderSelectionText(Graphics2D context, IRenderedRow row) {
! if (selectedRows.contains(row.getPayload())) {
! if (row.getHeight() != rendererResult.getHeight()) {
! Color highlight = new Color(0, 206, 209, 20);
! context.setColor(highlight);
! context.fillRect(0, 0, axisWidth, row.getHeight());
! } else {
! selectedRows.clear();
! }
! } else {
! List<IRenderedRow> subdivision = row.getNestedRows();
! if (subdivision.isEmpty()) {
! dimRect(context, 0, axisWidth, row.getHeight());
! } else {
! for (IRenderedRow nestedRow : row.getNestedRows()) {
! renderSelectionText(context, nestedRow);
! }
! return;
! }
! }
! context.translate(0, row.getHeight());
! }
!
! private void renderSelectionChart(Graphics2D context, IRenderedRow row) {
if (selectedRows.contains(row.getPayload())) {
renderSelection(context, xBucketRange, row.getHeight());
} else {
List<IRenderedRow> subdivision = row.getNestedRows();
if (subdivision.isEmpty()) {
dimRect(context, 0, axisWidth, row.getHeight());
} else {
for (IRenderedRow nestedRow : row.getNestedRows()) {
! renderSelectionChart(context, nestedRow);
}
return;
}
}
context.translate(0, row.getHeight());
}
+ // Paint the background of every-other row in a slightly different shade
+ // to better differentiate the thread lanes from one another
+ private void paintRowBackground(Graphics2D context, int height) {
+ if (rowColorCounter >= 0) {
+ if (rowColorCounter % 2 == 0) {
+ context.setColor(Palette.PF_BLACK_100.getAWTColor());
+ } else {
+ context.setColor(Palette.PF_BLACK_200.getAWTColor());
+ }
+ context.fillRect(0, 0, axisWidth, height);
+ rowColorCounter++;
+ }
+ }
+
private void renderText(Graphics2D context, IRenderedRow row) {
String text = row.getName();
int height = row.getHeight();
if (height >= context.getFontMetrics().getHeight()) {
if (text != null) {
+ paintRowBackground(context, row.getHeight());
context.setColor(Color.BLACK);
! context.drawLine(0, height - 1, axisWidth -15, height - 1);
! int y = ((height - context.getFontMetrics().getHeight()) / 2) + context.getFontMetrics().getAscent();
int charsWidth = context.getFontMetrics().charsWidth(text.toCharArray(), 0, text.length());
! if (xOffset > 0 && charsWidth > xOffset) {
float fitRatio = ((float) xOffset) / (charsWidth
+ context.getFontMetrics().charsWidth(ELLIPSIS.toCharArray(), 0, ELLIPSIS.length()));
text = text.substring(0, ((int) (text.length() * fitRatio)) - 1) + ELLIPSIS;
}
! context.drawString(text, 2, y);
} else {
List<IRenderedRow> subdivision = row.getNestedRows();
if (!subdivision.isEmpty()) {
for (IRenderedRow nestedRow : row.getNestedRows()) {
renderText(context, nestedRow);
*** 237,246 ****
--- 342,354 ----
*
* @param rightPercent
* @return true if the bounds changed. That is, if a redraw is required.
*/
public boolean pan(int rightPercent) {
+ if (rangeDuration != null) {
+ return panRange(Integer.signum(rightPercent));
+ }
if (xBucketRange != null) {
IQuantity oldStart = currentStart;
IQuantity oldEnd = currentEnd;
if (rightPercent > 0) {
currentEnd = QuantitiesToolkit
*** 258,273 ****
--- 366,419 ----
// Return true since a redraw forces creation of xBucketRange.
return true;
}
/**
+ * Pan the view at a rate relative the current zoom level.
+ * @param panDirection -1 to pan left, 1 to pan right
+ * @return true if the chart needs to be redrawn
+ */
+ public boolean panRange(int panDirection) {
+ if (zoomSteps == 0 || panDirection == 0 ||
+ (currentStart.compareTo(start) == 0 && panDirection == -1) ||
+ (currentEnd.compareTo(end) == 0 && panDirection == 1)) {
+ return false;
+ }
+
+ IQuantity panDiff = rangeDuration.multiply(panDirection * zoomPanPower);
+ IQuantity newStart = currentStart.in(UnitLookup.EPOCH_NS).add(panDiff);
+ IQuantity newEnd = currentEnd.in(UnitLookup.EPOCH_NS).add(panDiff);
+
+ // if panning would flow over the recording range start or end time,
+ // calculate the difference and add it so the other side.
+ if (newStart.compareTo(start) < 0) {
+ IQuantity diff = start.subtract(newStart);
+ newStart = start;
+ newEnd = newEnd.add(diff);
+ } else if (newEnd.compareTo(end) > 0) {
+ IQuantity diff = newEnd.subtract(end);
+ newStart = newStart.add(diff);
+ newEnd = end;
+ }
+ currentStart = newStart;
+ currentEnd = newEnd;
+ filterBar.setStartTime(currentStart);
+ filterBar.setEndTime(currentEnd);
+ isZoomCalculated = true;
+ return true;
+ }
+
+ /**
* Zoom the view.
*
* @param zoomInSteps
* @return true if the bounds changed. That is, if a redraw is required.
*/
public boolean zoom(int zoomInSteps) {
+ if (rangeDuration != null) {
+ return zoomRange(zoomInSteps);
+ }
return zoomXAxis(axisWidth / 2, zoomInSteps);
}
/**
* Zoom the view.
*** 278,287 ****
--- 424,434 ----
*/
public boolean zoom(int x, int zoomInSteps) {
return zoomXAxis(x - xOffset, zoomInSteps);
}
+ // Default zoom mechanics
private boolean zoomXAxis(int x, int zoomInSteps) {
if (xBucketRange == null) {
// Return true since a redraw forces creation of xBucketRange.
return true;
}
*** 297,307 ****
--- 444,598 ----
return (currentStart.compareTo(oldStart) != 0) || (currentEnd.compareTo(oldEnd) != 0);
}
return false;
}
+ /**
+ * Zoom based on a percentage of the recording range
+ * @param zoomInSteps
+ * @return true if a redraw is required as a result of a successful zoom
+ */
+ public boolean zoomRange(int zoomInSteps) {
+ if (zoomInSteps > 0) {
+ zoomIn();
+ } else {
+ if (zoomSteps == 0) {
+ return false;
+ }
+ zoomOut();
+ }
+ // set displayBar text
+ displayBar.setZoomPercentageText(currentZoom);
+ return true;
+ }
+
+ /**
+ * Zoom into the chart at a rate of 5% of the overall recording range at each step.
+ * If the chart is zoomed in far enough such that one more step at 5% is not possible,
+ * the zoom power is halved and the zoom will proceed.
+ * <br>
+ * Every time the zoom power is halved, the instigating step value is pushed onto the
+ * modifiedSteps stack. This stack is consulted on zoom out events in order to ensure
+ * the chart zooms out the same way it was zoomed in.
+ */
+ private void zoomIn() {
+ IQuantity zoomDiff = rangeDuration.multiply(zoomPanPower);
+ IQuantity newStart = currentStart.in(UnitLookup.EPOCH_NS).add(zoomDiff);
+ IQuantity newEnd = currentEnd.in(UnitLookup.EPOCH_NS).subtract(zoomDiff);
+ if (newStart.compareTo(newEnd) >= 0) { // adjust the zoom factor
+ if (modifiedSteps == null) {
+ modifiedSteps = new Stack<Integer>();
+ }
+ modifiedSteps.push(zoomSteps);
+ zoomPanPower = zoomPanPower / ZOOM_PAN_MODIFIER;
+ zoomDiff = rangeDuration.multiply(zoomPanPower);
+ newStart = currentStart.in(UnitLookup.EPOCH_NS).add(zoomDiff);
+ newEnd = currentEnd.in(UnitLookup.EPOCH_NS).subtract(zoomDiff);
+ }
+ currentZoom = currentZoom + (zoomPanPower * ZOOM_PAN_MODIFIER * 100);
+ isZoomCalculated = true;
+ setVisibleRange(newStart, newEnd);
+ zoomSteps++;
+ }
+
+ /**
+ * Zoom out of the chart at a rate equal to the how the chart was zoomed in.
+ */
+ private void zoomOut() {
+ if (modifiedSteps != null && modifiedSteps.size() > 0 && modifiedSteps.peek() == zoomSteps) {
+ modifiedSteps.pop();
+ zoomPanPower = zoomPanPower * ZOOM_PAN_MODIFIER;
+ }
+ IQuantity zoomDiff = rangeDuration.multiply(zoomPanPower);
+ IQuantity newStart = currentStart.in(UnitLookup.EPOCH_NS).subtract(zoomDiff);
+ IQuantity newEnd = currentEnd.in(UnitLookup.EPOCH_NS).add(zoomDiff);
+
+ // if zooming out would flow over the recording range start or end time,
+ // calculate the difference and add it to the other side.
+ if (newStart.compareTo(start) < 0) {
+ IQuantity diff = start.subtract(newStart);
+ newStart = start;
+ newEnd = newEnd.add(diff);
+ } else if (newEnd.compareTo(end) > 0) {
+ IQuantity diff = newEnd.subtract(end);
+ newStart = newStart.subtract(diff);
+ newEnd = end;
+ }
+ currentZoom = currentZoom - (zoomPanPower * ZOOM_PAN_MODIFIER * 100);
+ if (currentZoom < 100) {
+ currentZoom = 100;
+ }
+ isZoomCalculated = true;
+ setVisibleRange(newStart, newEnd);
+ zoomSteps--;
+ }
+
+ // need to check from ChartAndPopupTableUI if not using the OG start/end position,
+ // will have to calculate the new zoom level
+ public void resetZoomFactor() {
+ zoomSteps = 0;
+ zoomPanPower = ZOOM_PAN_FACTOR / ZOOM_PAN_MODIFIER;
+ currentZoom = 100;
+ displayBar.setZoomPercentageText(currentZoom);
+ modifiedSteps = new Stack<Integer>();
+ }
+
+ /**
+ * Reset the visible range to be the recording range, and reset the zoom-related objects
+ */
+ public void resetTimeline() {
+ resetZoomFactor();
+ setVisibleRange(start, end);
+ }
+
+ private void selectionZoom(IQuantity newStart, IQuantity newEnd) {
+ double percentage = calculateZoom(newStart, newEnd);
+ zoomSteps = calculateZoomSteps(percentage);
+ currentZoom = 100 + (percentage * 100);
+ displayBar.setScaleValue(zoomSteps);
+ displayBar.setZoomPercentageText(currentZoom);
+ }
+
+ /**
+ * When a drag-select zoom occurs, use the new range value to determine how many steps have been taken,
+ * and adjust zoomSteps and zoomPower accordingly
+ */
+ private double calculateZoom(IQuantity newStart, IQuantity newEnd) {
+ // calculate the new visible range, and it's percentage of the total range
+ IQuantity newRange = newEnd.in(UnitLookup.EPOCH_NS).subtract(newStart.in(UnitLookup.EPOCH_NS));
+ return 1 - (newRange.longValue() / (double) rangeDuration.in(UnitLookup.NANOSECOND).longValue());
+ }
+
+ /**
+ * Calculate the number of steps required to achieve the passed zoom percentage
+ */
+ private int calculateZoomSteps(double percentage) {
+ double tempPercent = 0;
+ int steps = 0;
+ do {
+ tempPercent = tempPercent + ZOOM_PAN_FACTOR;
+ steps++;
+ } while (tempPercent <= percentage);
+ return steps;
+ }
+
+ private boolean isZoomCalculated;
+ private boolean isZoomPanDrag;
+
+ public void setIsZoomPanDrag(boolean isZoomPanDrag) {
+ this.isZoomPanDrag = isZoomPanDrag;
+ }
+
+ private boolean getIsZoomPanDrag() {
+ return isZoomPanDrag;
+ }
+
public void setVisibleRange(IQuantity rangeStart, IQuantity rangeEnd) {
+ if (rangeDuration != null && !isZoomCalculated && rangeStart != start && !getIsZoomPanDrag()) {
+ selectionZoom(rangeStart, rangeEnd);
+ }
+ isZoomCalculated = false;
rangeStart = QuantitiesToolkit.max(rangeStart, start);
rangeEnd = QuantitiesToolkit.min(rangeEnd, end);
if (rangeStart.compareTo(rangeEnd) < 0) {
SubdividedQuantityRange testRange = new SubdividedQuantityRange(rangeStart, rangeEnd, 10000, 1);
if (testRange.getQuantityAtPixel(0).compareTo(testRange.getQuantityAtPixel(1)) < 0) {
*** 310,319 ****
--- 601,614 ----
} else {
// Ensures that zoom out is always allowed
currentStart = QuantitiesToolkit.min(rangeStart, currentStart);
currentEnd = QuantitiesToolkit.max(rangeEnd, currentEnd);
}
+ if (filterBar != null) {
+ filterBar.setStartTime(currentStart);
+ filterBar.setEndTime(currentEnd);
+ }
rangeListeners.stream().forEach(l -> l.accept(getVisibleRange()));
}
}
private List<Consumer<IRange<IQuantity>>> rangeListeners = new ArrayList<>();
*** 330,363 ****
public void clearVisibleRange() {
currentStart = start;
currentEnd = end;
}
! public boolean select(int x1, int x2, int y1, int y2) {
! int xStart = Math.min(x1, x2) - xOffset;
! int xEnd = Math.max(x1, x2) - xOffset;
!
! if (xBucketRange != null && (xEnd >= 0)) {
! return select(xBucketRange.getQuantityAtPixel(Math.max(0, xStart)), xBucketRange.getQuantityAtPixel(xEnd),
! y1, y2);
} else {
! return select(null, null, y1, y2);
}
}
! public boolean select(IQuantity xStart, IQuantity xEnd, int y1, int y2) {
if (xStart != null && xStart.compareTo(start) < 0) {
xStart = start;
}
if (xEnd != null && xEnd.compareTo(end) > 0) {
xEnd = end;
}
Set<Object> oldRows = null;
if (QuantitiesToolkit.same(selectionStart, xStart) && QuantitiesToolkit.same(selectionEnd, xEnd)) {
oldRows = new HashSet<>(selectedRows);
}
selectedRows.clear();
addSelectedRows(rendererResult, 0, Math.min(y1, y2), Math.max(y1, y2));
selectionStart = xStart;
selectionEnd = xEnd;
return (oldRows == null) || !oldRows.equals(selectedRows);
}
--- 625,660 ----
public void clearVisibleRange() {
currentStart = start;
currentEnd = end;
}
! public boolean select(int x1, int x2, int y1, int y2, boolean clear) {
! int xStart = Math.min(x1, x2);
! int xEnd = Math.max(x1, x2);
!
! if (xBucketRange != null && (xEnd != xStart) && xEnd - xOffset >= 0) {
! return select(xBucketRange.getQuantityAtPixel(Math.max(0, xStart - xOffset)), xBucketRange.getQuantityAtPixel(xEnd - xOffset),
! y1, y2, clear);
} else {
! return select(null, null, y1, y2, clear);
}
}
! public boolean select(IQuantity xStart, IQuantity xEnd, int y1, int y2, boolean clear) {
if (xStart != null && xStart.compareTo(start) < 0) {
xStart = start;
}
if (xEnd != null && xEnd.compareTo(end) > 0) {
xEnd = end;
}
Set<Object> oldRows = null;
if (QuantitiesToolkit.same(selectionStart, xStart) && QuantitiesToolkit.same(selectionEnd, xEnd)) {
oldRows = new HashSet<>(selectedRows);
}
+ if (clear) {
selectedRows.clear();
+ }
addSelectedRows(rendererResult, 0, Math.min(y1, y2), Math.max(y1, y2));
selectionStart = xStart;
selectionEnd = xEnd;
return (oldRows == null) || !oldRows.equals(selectedRows);
}
*** 370,380 ****
selectionStart = selectionEnd = null;
return true;
}
private boolean addSelectedRows(IRenderedRow row, int yRowStart, int ySelectionStart, int ySelectionEnd) {
! List<IRenderedRow> subdivision = row.getNestedRows();
if (subdivision.isEmpty()) {
return addPayload(row);
} else {
boolean nestedHasPayload = false;
for (IRenderedRow nestedRow : row.getNestedRows()) {
--- 667,677 ----
selectionStart = selectionEnd = null;
return true;
}
private boolean addSelectedRows(IRenderedRow row, int yRowStart, int ySelectionStart, int ySelectionEnd) {
! List<IRenderedRow> subdivision = row.getNestedRows(); // height 1450, has all 32 rows
if (subdivision.isEmpty()) {
return addPayload(row);
} else {
boolean nestedHasPayload = false;
for (IRenderedRow nestedRow : row.getNestedRows()) {
*** 391,401 ****
--- 688,702 ----
}
private boolean addPayload(IRenderedRow row) {
Object payload = row.getPayload();
if (payload != null) {
+ if (selectedRows.contains(payload)) { // ctrl+click deselection
+ selectedRows.remove(payload);
+ } else {
selectedRows.add(payload);
+ }
return true;
}
return false;
}
*** 429,439 ****
}
}
private static void dimRect(Graphics2D context, int from, int width, int height) {
context.setColor(SELECTION_COLOR);
! context.fillRect(from, 0, width, height);
}
/**
* Let the {@code visitor} visit the chart elements in the vicinity of {@code x} and {@code y}.
* The visitation should adhere to a basic front to back ordering, so that elements which
--- 730,740 ----
}
}
private static void dimRect(Graphics2D context, int from, int width, int height) {
context.setColor(SELECTION_COLOR);
! context.fillRect(from , 0, width, height);
}
/**
* Let the {@code visitor} visit the chart elements in the vicinity of {@code x} and {@code y}.
* The visitation should adhere to a basic front to back ordering, so that elements which
< prev index next >