aboutsummaryrefslogtreecommitdiff
path: root/ui/src/frontend/gridline_helper.ts
blob: b61729e43a095236cb2d2f10e0f18f38007f8c3f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
// Copyright (C) 2018 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import {TimeSpan} from '../common/time';

import {TRACK_BORDER_COLOR, TRACK_SHELL_WIDTH} from './css_constants';
import {TimeScale} from './time_scale';

export const DESIRED_PX_PER_STEP = 80;

/**
 * Returns the step size of a grid line in seconds.
 * The returned step size has two properties:
 * (1) It is 1, 2, or 5, multiplied by some integer power of 10.
 * (2) The number steps in |range| produced by |stepSize| is as close as
 *     possible to |desiredSteps|.
 */
export function getGridStepSize(range: number, desiredSteps: number): number {
  // First, get the largest possible power of 10 that is smaller than the
  // desired step size, and set it to the current step size.
  // For example, if the range is 2345ms and the desired steps is 10, then the
  // desired step size is 234.5 and the step size will be set to 100.
  const desiredStepSize = range / desiredSteps;
  const zeros = Math.floor(Math.log10(desiredStepSize));
  const initialStepSize = Math.pow(10, zeros);

  // This function first calculates how many steps within the range a certain
  // stepSize will produce, and returns the difference between that and
  // desiredSteps.
  const distToDesired = (evaluatedStepSize: number) =>
      Math.abs(range / evaluatedStepSize - desiredSteps);

  // We know that |initialStepSize| is a power of 10, and
  // initialStepSize <= desiredStepSize <= 10 * initialStepSize. There are four
  // possible candidates for final step size: 1, 2, 5 or 10 * initialStepSize.
  // We pick the candidate that minimizes distToDesired(stepSize).
  const stepSizeMultipliers = [2, 5, 10];

  let minimalDistance = distToDesired(initialStepSize);
  let minimizingStepSize = initialStepSize;

  for (const multiplier of stepSizeMultipliers) {
    const newStepSize = multiplier * initialStepSize;
    const newDistance = distToDesired(newStepSize);
    if (newDistance < minimalDistance) {
      minimalDistance = newDistance;
      minimizingStepSize = newStepSize;
    }
  }
  return minimizingStepSize;
}

/**
 * Generator that returns that (given a width im px, span, and scale) returns
 * pairs of [xInPx, timestampInS] pairs describing where gridlines should be
 * drawn.
 */
export function gridlines(width: number, span: TimeSpan, timescale: TimeScale):
    Array<[number, number]> {
  const desiredSteps = width / DESIRED_PX_PER_STEP;
  const step = getGridStepSize(span.duration, desiredSteps);
  const start = Math.round(span.start / step) * step;
  const lines: Array<[number, number]> = [];
  let previousTimestamp = Number.NEGATIVE_INFINITY;
  // Iterating over the number of steps instead of
  // for (let s = start; s < span.end; s += step) because if start is very large
  // number and step very small, s will never reach end.
  for (let i = 0; i < desiredSteps; i++) {
    let xPos = TRACK_SHELL_WIDTH;
    const timestamp = start + i * step;
    xPos += Math.floor(timescale.timeToPx(timestamp));
    if (xPos < TRACK_SHELL_WIDTH) continue;
    if (xPos > width) break;
    if (Math.abs(timestamp - previousTimestamp) > Number.EPSILON) {
      previousTimestamp = timestamp;
      lines.push([xPos, timestamp]);
    }
  }
  return lines;
}

export function drawGridLines(
    ctx: CanvasRenderingContext2D,
    x: TimeScale,
    timeSpan: TimeSpan,
    width: number,
    height: number): void {
  ctx.strokeStyle = TRACK_BORDER_COLOR;
  ctx.lineWidth = 1;

  for (const xAndTime of gridlines(width, timeSpan, x)) {
    ctx.beginPath();
    ctx.moveTo(xAndTime[0] + 0.5, 0);
    ctx.lineTo(xAndTime[0] + 0.5, height);
    ctx.stroke();
  }
}