in adt-ui/src/main/java/com/android/tools/adtui/chart/linechart/LineChart.java [190:393]
private void redraw(@NotNull Dimension dim) {
long duration = System.nanoTime();
// Store the last stacked series to use them to increment the Y values
// of the current stacked series.
List<SeriesData<Long>> lastStackedSeries = null;
Deque<Path2D> orderedPaths = new ArrayDeque<>(myLinesConfig.size());
Deque<RangedContinuousSeries> orderedSeries = new ArrayDeque<>(myLinesConfig.size());
for (RangedContinuousSeries ranged : myModel.getSeries()) {
if (ranged.getXRange().isEmpty() || ranged.getXRange().isPoint()
|| ranged.getYRange().isEmpty() || ranged.getYRange().isPoint()) {
continue;
}
final LineConfig config = getLineConfig(ranged);
List<SeriesData<Long>> seriesList = ranged.getSeries();
if (config.isStacked()) {
if (lastStackedSeries == null) {
// Create a new list of SeriesData to prevent modifying the backing data series, which could be cached.
lastStackedSeries = ContainerUtil.map(seriesList, data -> new SeriesData<>(data.x, data.value));
}
else {
// If the current series is stacked, increment its value by the value of the last stacked
// series. As the series are constantly populated, the current series might have more
// points than the last stacked series (meaning that the last one was populated in a
// prior iteration). In this case, ignore the new points (i.e. we take only the intersection
// across all series).
for (int i = 0; i < seriesList.size() && i < lastStackedSeries.size(); ++i) {
// An assumption is made here that the x values across series are aligned.
lastStackedSeries.get(i).value += seriesList.get(i).value;
}
seriesList = lastStackedSeries;
}
}
Path2D path = new Path2D.Float();
double xMin = ranged.getXRange().getMin();
double xLength = ranged.getXRange().getLength();
double yMin = ranged.getYRange().getMin();
double yLength = ranged.getYRange().getLength();
// X coordinate of the first point
double firstXd = 0f;
// Actual value of first point
double firstX = 0;
seriesList = myReducer.reduceData(seriesList, config);
double xBucketInterval = config.getDataBucketInterval() / xLength;
double xBucketBarWidth = xBucketInterval * BUCKET_BAR_PERCENTAGE;
// If we are a stepped chart or bar chart, we don't need to worry about start and end points' Y value.
boolean optimizeYZooming = !config.isStepped() && xBucketInterval == 0;
for (int i = 0; i < seriesList.size(); i++) {
SeriesData<Long> data = seriesList.get(i);
SeriesData<Long> dataNext = seriesList.get(i + 1 == seriesList.size() ? i : i + 1);
SeriesData<Long> dataPrev = seriesList.get(i - 1 < 0 ? i : i - 1);
// TODO: refactor to allow different types (e.g. double)
double xd = (data.x - xMin) / xLength;
// Swing's (0, 0) coordinate is in top-left. As we use bottom-left (0, 0), we need to adjust the y coordinate.
double yd = 1 - (data.value - yMin) / yLength;
// This change significantly speeds up drawing when zoomed into the chart. Without this change a line could extend
// a few thousand pixels off the screen in both directions. The fill/draw function would then spend a lot of time
// computing a line fill for pixels never to be rendered.
// Truncate points that are off screen. Ones that cross the border get pushed to the border, and the
// height gets scaled accordingly.
// For example, two out of bounds points would get snapped into place.
// | |
// | * <-- *
// * --> * |
// | |
// X Axis: -------|--------------------|------
// 0 1
double originalXd = xd;
if (xd < 0) {
double xdNext = (dataNext.x - xMin) / xLength;
// If our next point is also offscreen then ignore this point and continue.
if (xdNext < 0) {
if (data == dataNext) {
// The last point is still off screen, we should add a point at (0, y) to avoid drawing nothing.
// | |
// *-->*----
// | |
// 0 1
path.moveTo(0f, yd);
}
continue;
}
//Get the Y offset of our next point.
double ydNext = 1 - (dataNext.value - yMin) / yLength;
// If we are a dash line we get the closest normalized point to are graph otherwise we just set our point to 0.
double newPosition = 0;
if (config.isDash()) {
newPosition = xd % 1.0f;
}
if (optimizeYZooming) {
// If we are not stepped we need to adjust the starting Y position to be a linear interpolation of our new X point.
double ratio = (newPosition - xd) / (xdNext - xd);
yd = (1 - ratio) * yd + (ratio * ydNext);
}
// Set our new X position and carry on.
xd = newPosition;
}
else if (xd > 1) {
double xdPrev = (dataPrev.x - xMin) / xLength;
if (xdPrev > 1) {
break;
}
if (optimizeYZooming) {
double ratio = (1 - xdPrev) / (xd - xdPrev);
double ydPrev = 1 - (dataPrev.value - yMin) / yLength;
yd = (1 - ratio) * ydPrev + (ratio * yd);
}
xd = 1;
}
if (path.getCurrentPoint() == null) {
firstXd = xd;
firstX = data.x;
// If for bucket data, because the previous ending x value is next data point's starting
// x value, i.e. (xd + interval, 1), move the path start point to (xd, 1).
// Otherwise, move the path start point to (xd, yd).
path.moveTo(xd, xBucketInterval != 0 ? 1 : yd);
}
else if (xBucketInterval == 0) {
// If the chart is stepped, a horizontal line should be drawn from the current
// point (e.g. (x0, y0)) to the destination's X value (e.g. (x1, y0)) before
// drawing a line to the destination point itself (e.g. (x1, y1)).
if (config.isStepped()) {
float y = (float)path.getCurrentPoint().getY();
path.lineTo(xd, y);
}
path.lineTo(xd, yd);
}
if (xBucketInterval != 0) {
// Move line to (xd, 1) first because data points may not be equal time buckets, for example, data points are (1000, 1),
// (1200, 2), (2000, 3).
double barX = Math.min(1, originalXd + xBucketBarWidth);
if (barX - xd > EPSILON) {
path.lineTo(xd, 1);
path.lineTo(xd, yd);
path.lineTo(barX, yd);
path.lineTo(barX, 1);
}
}
}
if (path.getCurrentPoint() != null) {
// Extends the last point on the path to the end
path.lineTo(Math.max(path.getCurrentPoint().getX(), myFillEndSupplier.getAsDouble()), path.getCurrentPoint().getY());
}
if (config.isFilled() && path.getCurrentPoint() != null) {
// If the chart is filled, draw a line from the last point to X
// axis and another one from this new point to the first destination point.
path.lineTo(path.getCurrentPoint().getX(), 1f);
path.lineTo(firstXd, 1f);
}
if (config.isFilled()) {
// Draw the filled lines first, otherwise other lines won't be visible.
// Also, to draw stacked and filled lines correctly, they need to be drawn in reverse order to their adding order.
orderedPaths.addFirst(path);
orderedSeries.addFirst(ranged);
}
else {
orderedPaths.addLast(path);
orderedSeries.addLast(ranged);
}
if (config.isDash() && config.isAdjustDash()) {
DashInfo dashInfo;
if (!myDashInfoCache.containsKey(config)) {
dashInfo = new DashInfo();
myDashInfoCache.put(config, dashInfo);
// No previous dataInfo so don't bother trying to adjust dash phase.
}
else {
dashInfo = myDashInfoCache.get(config);
computeAdjustedDashPhase(dashInfo, config, path, dim, firstX, xMin, xLength, yLength);
}
dashInfo.myPreviousFirstX = firstX;
dashInfo.myPreviousXMin = xMin;
dashInfo.myPreviousXLength = xLength;
dashInfo.myPreviousYLength = yLength;
dashInfo.myPreviousDashPath = path;
}
else {
myDashInfoCache.remove(config);
}
}
myLinePaths.clear();
myLinePaths.addAll(orderedPaths);
myLinePathSeries.clear();
myLinePathSeries.addAll(orderedSeries);
addDebugInfo("postAnimate time: %d ms", TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - duration));
}