Michail Schwab | a86aae4 | 2018-07-20 11:58:28 -0400 | [diff] [blame] | 1 | // 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 Golton | 9ae7558 | 2023-01-26 18:48:16 +0000 | [diff] [blame] | 15 | import {assertTrue} from '../base/logging'; |
Steve Golton | 278b7f0 | 2023-09-06 16:26:23 +0100 | [diff] [blame] | 16 | import {duration, Span, time, Time} from '../base/time'; |
Steve Golton | f3897e2 | 2023-05-11 14:18:30 +0100 | [diff] [blame] | 17 | |
Isabelle Taylor | a16dec2 | 2019-12-03 16:34:13 +0000 | [diff] [blame] | 18 | import {TRACK_BORDER_COLOR, TRACK_SHELL_WIDTH} from './css_constants'; |
Steve Golton | 9ae7558 | 2023-01-26 18:48:16 +0000 | [diff] [blame] | 19 | import {globals} from './globals'; |
Primiano Tucci | f30cd9c | 2018-08-13 01:53:26 +0200 | [diff] [blame] | 20 | import {TimeScale} from './time_scale'; |
Michail Schwab | a86aae4 | 2018-07-20 11:58:28 -0400 | [diff] [blame] | 21 | |
Steve Golton | f3897e2 | 2023-05-11 14:18:30 +0100 | [diff] [blame] | 22 | const micros = 1000n; |
| 23 | const millis = 1000n * micros; |
| 24 | const seconds = 1000n * millis; |
| 25 | const minutes = 60n * seconds; |
| 26 | const hours = 60n * minutes; |
| 27 | const days = 24n * hours; |
Michail Schwab | 7e4b89e | 2018-07-27 10:48:40 -0400 | [diff] [blame] | 28 | |
Steve Golton | f3897e2 | 2023-05-11 14:18:30 +0100 | [diff] [blame] | 29 | // These patterns cover the entire range of 0 - 2^63-1 nanoseconds |
| 30 | const 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 Schwab | 7e4b89e | 2018-07-27 10:48:40 -0400 | [diff] [blame] | 90 | |
Steve Golton | f3897e2 | 2023-05-11 14:18:30 +0100 | [diff] [blame] | 91 | // Returns the optimal step size and pattern of ticks within the step. |
Steve Golton | b3a389d | 2023-07-10 11:03:17 +0100 | [diff] [blame] | 92 | export function getPattern(minPatternSize: bigint): [duration, string] { |
Steve Golton | f3897e2 | 2023-05-11 14:18:30 +0100 | [diff] [blame] | 93 | for (const [size, pattern] of patterns) { |
| 94 | if (size >= minPatternSize) { |
| 95 | return [size, pattern]; |
Michail Schwab | a86aae4 | 2018-07-20 11:58:28 -0400 | [diff] [blame] | 96 | } |
Michail Schwab | a86aae4 | 2018-07-20 11:58:28 -0400 | [diff] [blame] | 97 | } |
Steve Golton | 9ae7558 | 2023-01-26 18:48:16 +0000 | [diff] [blame] | 98 | |
Steve Golton | f3897e2 | 2023-05-11 14:18:30 +0100 | [diff] [blame] | 99 | throw new Error('Pattern not defined for this minsize'); |
Hector Dearman | 62b3a89 | 2019-01-10 13:25:55 +0000 | [diff] [blame] | 100 | } |
Hector Dearman | ea002ea | 2019-01-21 11:43:45 +0000 | [diff] [blame] | 101 | |
Steve Golton | 9ae7558 | 2023-01-26 18:48:16 +0000 | [diff] [blame] | 102 | function 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 Golton | 9ae7558 | 2023-01-26 18:48:16 +0000 | [diff] [blame] | 119 | export enum TickType { |
| 120 | MAJOR, |
| 121 | MEDIUM, |
| 122 | MINOR |
| 123 | } |
| 124 | |
| 125 | export interface Tick { |
| 126 | type: TickType; |
Steve Golton | b3a389d | 2023-07-10 11:03:17 +0100 | [diff] [blame] | 127 | time: time; |
Steve Golton | 9ae7558 | 2023-01-26 18:48:16 +0000 | [diff] [blame] | 128 | } |
| 129 | |
Steve Golton | 5547f59 | 2023-06-12 09:14:40 +0100 | [diff] [blame] | 130 | export const MIN_PX_PER_STEP = 120; |
Steve Golton | f3897e2 | 2023-05-11 14:18:30 +0100 | [diff] [blame] | 131 | export function getMaxMajorTicks(width: number) { |
| 132 | return Math.max(1, Math.floor(width / MIN_PX_PER_STEP)); |
| 133 | } |
| 134 | |
Steve Golton | 9ae7558 | 2023-01-26 18:48:16 +0000 | [diff] [blame] | 135 | // An iterable which generates a series of ticks for a given timescale. |
| 136 | export class TickGenerator implements Iterable<Tick> { |
| 137 | private _tickPattern: TickType[]; |
Steve Golton | b3a389d | 2023-07-10 11:03:17 +0100 | [diff] [blame] | 138 | private _patternSize: duration; |
| 139 | private _timeSpan: Span<time, duration>; |
| 140 | private _offset: time; |
Steve Golton | 9ae7558 | 2023-01-26 18:48:16 +0000 | [diff] [blame] | 141 | |
Steve Golton | f3897e2 | 2023-05-11 14:18:30 +0100 | [diff] [blame] | 142 | constructor( |
Steve Golton | b3a389d | 2023-07-10 11:03:17 +0100 | [diff] [blame] | 143 | timeSpan: Span<time, duration>, maxMajorTicks: number, |
| 144 | offset: time = Time.ZERO) { |
Steve Golton | f3897e2 | 2023-05-11 14:18:30 +0100 | [diff] [blame] | 145 | assertTrue(timeSpan.duration > 0n, 'timeSpan.duration cannot be lte 0'); |
| 146 | assertTrue(maxMajorTicks > 0, 'maxMajorTicks cannot be lte 0'); |
Steve Golton | 9ae7558 | 2023-01-26 18:48:16 +0000 | [diff] [blame] | 147 | |
Steve Golton | f3897e2 | 2023-05-11 14:18:30 +0100 | [diff] [blame] | 148 | 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 Golton | 9ae7558 | 2023-01-26 18:48:16 +0000 | [diff] [blame] | 153 | 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 Golton | f3897e2 | 2023-05-11 14:18:30 +0100 | [diff] [blame] | 159 | // more elegant compared to creating an array of ticks and building an |
| 160 | // iterator for it. |
Steve Golton | 9ae7558 | 2023-01-26 18:48:16 +0000 | [diff] [blame] | 161 | * [Symbol.iterator](): Generator<Tick> { |
Steve Golton | f3897e2 | 2023-05-11 14:18:30 +0100 | [diff] [blame] | 162 | const stepSize = this._patternSize / BigInt(this._tickPattern.length); |
Steve Golton | b3a389d | 2023-07-10 11:03:17 +0100 | [diff] [blame] | 163 | const start = Time.quantFloor(this._timeSpan.start, this._patternSize); |
Steve Golton | f3897e2 | 2023-05-11 14:18:30 +0100 | [diff] [blame] | 164 | const end = this._timeSpan.end; |
| 165 | let patternIndex = 0; |
Steve Golton | 9ae7558 | 2023-01-26 18:48:16 +0000 | [diff] [blame] | 166 | |
Steve Golton | b3a389d | 2023-07-10 11:03:17 +0100 | [diff] [blame] | 167 | for (let time = start; time < end; |
| 168 | time = Time.add(time, stepSize), patternIndex++) { |
Steve Golton | f3897e2 | 2023-05-11 14:18:30 +0100 | [diff] [blame] | 169 | if (time >= this._timeSpan.start) { |
| 170 | patternIndex = patternIndex % this._tickPattern.length; |
| 171 | const type = this._tickPattern[patternIndex]; |
Steve Golton | b3a389d | 2023-07-10 11:03:17 +0100 | [diff] [blame] | 172 | yield {type, time: Time.add(time, this._offset)}; |
Steve Golton | 9ae7558 | 2023-01-26 18:48:16 +0000 | [diff] [blame] | 173 | } |
Neda Topoljanac | 708030d | 2019-11-19 17:21:34 +0000 | [diff] [blame] | 174 | } |
Hector Dearman | ea002ea | 2019-01-21 11:43:45 +0000 | [diff] [blame] | 175 | } |
Steve Golton | 9ae7558 | 2023-01-26 18:48:16 +0000 | [diff] [blame] | 176 | } |
| 177 | |
| 178 | // Gets the timescale associated with the current visible window. |
| 179 | export function timeScaleForVisibleWindow( |
| 180 | startPx: number, endPx: number): TimeScale { |
Steve Golton | f3897e2 | 2023-05-11 14:18:30 +0100 | [diff] [blame] | 181 | return globals.frontendLocalState.getTimeScale(startPx, endPx); |
Hector Dearman | ea002ea | 2019-01-21 11:43:45 +0000 | [diff] [blame] | 182 | } |
| 183 | |
| 184 | export function drawGridLines( |
| 185 | ctx: CanvasRenderingContext2D, |
Hector Dearman | ea002ea | 2019-01-21 11:43:45 +0000 | [diff] [blame] | 186 | width: number, |
| 187 | height: number): void { |
Hector Dearman | ccb0b79 | 2019-01-22 13:19:41 +0000 | [diff] [blame] | 188 | ctx.strokeStyle = TRACK_BORDER_COLOR; |
Hector Dearman | ea002ea | 2019-01-21 11:43:45 +0000 | [diff] [blame] | 189 | ctx.lineWidth = 1; |
| 190 | |
Steve Golton | ab88091 | 2023-06-28 15:47:23 +0100 | [diff] [blame] | 191 | const span = globals.frontendLocalState.visibleTimeSpan; |
Steve Golton | f3897e2 | 2023-05-11 14:18:30 +0100 | [diff] [blame] | 192 | 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 Golton | 21a2fc2 | 2023-07-12 07:07:58 +0100 | [diff] [blame] | 195 | const offset = globals.timestampOffset(); |
Steve Golton | 3738d9f | 2023-06-22 20:22:39 +0100 | [diff] [blame] | 196 | for (const {type, time} of new TickGenerator(span, maxMajorTicks, offset)) { |
Steve Golton | b3a389d | 2023-07-10 11:03:17 +0100 | [diff] [blame] | 197 | const px = Math.floor(map.timeToPx(time)); |
Steve Golton | 9ae7558 | 2023-01-26 18:48:16 +0000 | [diff] [blame] | 198 | if (type === TickType.MAJOR) { |
| 199 | ctx.beginPath(); |
Steve Golton | f3897e2 | 2023-05-11 14:18:30 +0100 | [diff] [blame] | 200 | ctx.moveTo(px + 0.5, 0); |
| 201 | ctx.lineTo(px + 0.5, height); |
Steve Golton | 9ae7558 | 2023-01-26 18:48:16 +0000 | [diff] [blame] | 202 | ctx.stroke(); |
| 203 | } |
| 204 | } |
Hector Dearman | ea002ea | 2019-01-21 11:43:45 +0000 | [diff] [blame] | 205 | } |
| 206 | } |