blob: 04c24b3670e1a4a3b7ec61ae5b6b437d2ffc27a5 [file] [log] [blame]
Michail Schwaba86aae42018-07-20 11:58:28 -04001// Copyright (C) 2018 The Android Open Source Project
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
Steve Golton9ae75582023-01-26 18:48:16 +000015import {assertTrue} from '../base/logging';
Steve Golton278b7f02023-09-06 16:26:23 +010016import {duration, Span, time, Time} from '../base/time';
Steve Goltonf3897e22023-05-11 14:18:30 +010017
Isabelle Taylora16dec22019-12-03 16:34:13 +000018import {TRACK_BORDER_COLOR, TRACK_SHELL_WIDTH} from './css_constants';
Steve Golton9ae75582023-01-26 18:48:16 +000019import {globals} from './globals';
Primiano Tuccif30cd9c2018-08-13 01:53:26 +020020import {TimeScale} from './time_scale';
Michail Schwaba86aae42018-07-20 11:58:28 -040021
Steve Goltonf3897e22023-05-11 14:18:30 +010022const micros = 1000n;
23const millis = 1000n * micros;
24const seconds = 1000n * millis;
25const minutes = 60n * seconds;
26const hours = 60n * minutes;
27const days = 24n * hours;
Michail Schwab7e4b89e2018-07-27 10:48:40 -040028
Steve Goltonf3897e22023-05-11 14:18:30 +010029// These patterns cover the entire range of 0 - 2^63-1 nanoseconds
30const patterns: [bigint, string][] = [
31 [1n, '|'],
32 [2n, '|:'],
33 [5n, '|....'],
34 [10n, '|....:....'],
35 [20n, '|.:.'],
36 [50n, '|....'],
37 [100n, '|....:....'],
38 [200n, '|.:.'],
39 [500n, '|....'],
40 [1n * micros, '|....:....'],
41 [2n * micros, '|.:.'],
42 [5n * micros, '|....'],
43 [10n * micros, '|....:....'],
44 [20n * micros, '|.:.'],
45 [50n * micros, '|....'],
46 [100n * micros, '|....:....'],
47 [200n * micros, '|.:.'],
48 [500n * micros, '|....'],
49 [1n * millis, '|....:....'],
50 [2n * millis, '|.:.'],
51 [5n * millis, '|....'],
52 [10n * millis, '|....:....'],
53 [20n * millis, '|.:.'],
54 [50n * millis, '|....'],
55 [100n * millis, '|....:....'],
56 [200n * millis, '|.:.'],
57 [500n * millis, '|....'],
58 [1n * seconds, '|....:....'],
59 [2n * seconds, '|.:.'],
60 [5n * seconds, '|....'],
61 [10n * seconds, '|....:....'],
62 [30n * seconds, '|.:.:.'],
63 [1n * minutes, '|.....'],
64 [2n * minutes, '|.:.'],
65 [5n * minutes, '|.....'],
66 [10n * minutes, '|....:....'],
67 [30n * minutes, '|.:.:.'],
68 [1n * hours, '|.....'],
69 [2n * hours, '|.:.'],
70 [6n * hours, '|.....'],
71 [12n * hours, '|.....:.....'],
72 [1n * days, '|.:.'],
73 [2n * days, '|.:.'],
74 [5n * days, '|....'],
75 [10n * days, '|....:....'],
76 [20n * days, '|.:.'],
77 [50n * days, '|....'],
78 [100n * days, '|....:....'],
79 [200n * days, '|.:.'],
80 [500n * days, '|....'],
81 [1000n * days, '|....:....'],
82 [2000n * days, '|.:.'],
83 [5000n * days, '|....'],
84 [10000n * days, '|....:....'],
85 [20000n * days, '|.:.'],
86 [50000n * days, '|....'],
87 [100000n * days, '|....:....'],
88 [200000n * days, '|.:.'],
89];
Michail Schwab7e4b89e2018-07-27 10:48:40 -040090
Steve Goltonf3897e22023-05-11 14:18:30 +010091// Returns the optimal step size and pattern of ticks within the step.
Steve Goltonb3a389d2023-07-10 11:03:17 +010092export function getPattern(minPatternSize: bigint): [duration, string] {
Steve Goltonf3897e22023-05-11 14:18:30 +010093 for (const [size, pattern] of patterns) {
94 if (size >= minPatternSize) {
95 return [size, pattern];
Michail Schwaba86aae42018-07-20 11:58:28 -040096 }
Michail Schwaba86aae42018-07-20 11:58:28 -040097 }
Steve Golton9ae75582023-01-26 18:48:16 +000098
Steve Goltonf3897e22023-05-11 14:18:30 +010099 throw new Error('Pattern not defined for this minsize');
Hector Dearman62b3a892019-01-10 13:25:55 +0000100}
Hector Dearmanea002ea2019-01-21 11:43:45 +0000101
Steve Golton9ae75582023-01-26 18:48:16 +0000102function tickPatternToArray(pattern: string): TickType[] {
103 const array = Array.from(pattern);
104 return array.map((char) => {
105 switch (char) {
106 case '|':
107 return TickType.MAJOR;
108 case ':':
109 return TickType.MEDIUM;
110 case '.':
111 return TickType.MINOR;
112 default:
113 // This is almost certainly a developer/fat-finger error
114 throw Error(`Invalid char "${char}" in pattern "${pattern}"`);
115 }
116 });
117}
118
Steve Golton9ae75582023-01-26 18:48:16 +0000119export enum TickType {
120 MAJOR,
121 MEDIUM,
122 MINOR
123}
124
125export interface Tick {
126 type: TickType;
Steve Goltonb3a389d2023-07-10 11:03:17 +0100127 time: time;
Steve Golton9ae75582023-01-26 18:48:16 +0000128}
129
Steve Golton5547f592023-06-12 09:14:40 +0100130export const MIN_PX_PER_STEP = 120;
Steve Goltonf3897e22023-05-11 14:18:30 +0100131export function getMaxMajorTicks(width: number) {
132 return Math.max(1, Math.floor(width / MIN_PX_PER_STEP));
133}
134
Steve Golton9ae75582023-01-26 18:48:16 +0000135// An iterable which generates a series of ticks for a given timescale.
136export class TickGenerator implements Iterable<Tick> {
137 private _tickPattern: TickType[];
Steve Goltonb3a389d2023-07-10 11:03:17 +0100138 private _patternSize: duration;
139 private _timeSpan: Span<time, duration>;
140 private _offset: time;
Steve Golton9ae75582023-01-26 18:48:16 +0000141
Steve Goltonf3897e22023-05-11 14:18:30 +0100142 constructor(
Steve Goltonb3a389d2023-07-10 11:03:17 +0100143 timeSpan: Span<time, duration>, maxMajorTicks: number,
144 offset: time = Time.ZERO) {
Steve Goltonf3897e22023-05-11 14:18:30 +0100145 assertTrue(timeSpan.duration > 0n, 'timeSpan.duration cannot be lte 0');
146 assertTrue(maxMajorTicks > 0, 'maxMajorTicks cannot be lte 0');
Steve Golton9ae75582023-01-26 18:48:16 +0000147
Steve Goltonf3897e22023-05-11 14:18:30 +0100148 this._timeSpan = timeSpan.add(-offset);
149 this._offset = offset;
150 const minStepSize =
151 BigInt(Math.floor(Number(timeSpan.duration) / maxMajorTicks));
152 const [size, pattern] = getPattern(minStepSize);
Steve Golton9ae75582023-01-26 18:48:16 +0000153 this._patternSize = size;
154 this._tickPattern = tickPatternToArray(pattern);
155 }
156
157 // Returns an iterable, so this object can be iterated over directly using the
158 // `for x of y` notation. The use of a generator here is just to make things
Steve Goltonf3897e22023-05-11 14:18:30 +0100159 // more elegant compared to creating an array of ticks and building an
160 // iterator for it.
Steve Golton9ae75582023-01-26 18:48:16 +0000161 * [Symbol.iterator](): Generator<Tick> {
Steve Goltonf3897e22023-05-11 14:18:30 +0100162 const stepSize = this._patternSize / BigInt(this._tickPattern.length);
Steve Goltonb3a389d2023-07-10 11:03:17 +0100163 const start = Time.quantFloor(this._timeSpan.start, this._patternSize);
Steve Goltonf3897e22023-05-11 14:18:30 +0100164 const end = this._timeSpan.end;
165 let patternIndex = 0;
Steve Golton9ae75582023-01-26 18:48:16 +0000166
Steve Goltonb3a389d2023-07-10 11:03:17 +0100167 for (let time = start; time < end;
168 time = Time.add(time, stepSize), patternIndex++) {
Steve Goltonf3897e22023-05-11 14:18:30 +0100169 if (time >= this._timeSpan.start) {
170 patternIndex = patternIndex % this._tickPattern.length;
171 const type = this._tickPattern[patternIndex];
Steve Goltonb3a389d2023-07-10 11:03:17 +0100172 yield {type, time: Time.add(time, this._offset)};
Steve Golton9ae75582023-01-26 18:48:16 +0000173 }
Neda Topoljanac708030d2019-11-19 17:21:34 +0000174 }
Hector Dearmanea002ea2019-01-21 11:43:45 +0000175 }
Steve Golton9ae75582023-01-26 18:48:16 +0000176}
177
178// Gets the timescale associated with the current visible window.
179export function timeScaleForVisibleWindow(
180 startPx: number, endPx: number): TimeScale {
Steve Goltonf3897e22023-05-11 14:18:30 +0100181 return globals.frontendLocalState.getTimeScale(startPx, endPx);
Hector Dearmanea002ea2019-01-21 11:43:45 +0000182}
183
184export function drawGridLines(
185 ctx: CanvasRenderingContext2D,
Hector Dearmanea002ea2019-01-21 11:43:45 +0000186 width: number,
187 height: number): void {
Hector Dearmanccb0b792019-01-22 13:19:41 +0000188 ctx.strokeStyle = TRACK_BORDER_COLOR;
Hector Dearmanea002ea2019-01-21 11:43:45 +0000189 ctx.lineWidth = 1;
190
Steve Goltonab880912023-06-28 15:47:23 +0100191 const span = globals.frontendLocalState.visibleTimeSpan;
Steve Goltonf3897e22023-05-11 14:18:30 +0100192 if (width > TRACK_SHELL_WIDTH && span.duration > 0n) {
193 const maxMajorTicks = getMaxMajorTicks(width - TRACK_SHELL_WIDTH);
194 const map = timeScaleForVisibleWindow(TRACK_SHELL_WIDTH, width);
Steve Golton21a2fc22023-07-12 07:07:58 +0100195 const offset = globals.timestampOffset();
Steve Golton3738d9f2023-06-22 20:22:39 +0100196 for (const {type, time} of new TickGenerator(span, maxMajorTicks, offset)) {
Steve Goltonb3a389d2023-07-10 11:03:17 +0100197 const px = Math.floor(map.timeToPx(time));
Steve Golton9ae75582023-01-26 18:48:16 +0000198 if (type === TickType.MAJOR) {
199 ctx.beginPath();
Steve Goltonf3897e22023-05-11 14:18:30 +0100200 ctx.moveTo(px + 0.5, 0);
201 ctx.lineTo(px + 0.5, height);
Steve Golton9ae75582023-01-26 18:48:16 +0000202 ctx.stroke();
203 }
204 }
Hector Dearmanea002ea2019-01-21 11:43:45 +0000205 }
206}