diff options
Diffstat (limited to 'ui/src/frontend/gridline_helper.ts')
-rw-r--r-- | ui/src/frontend/gridline_helper.ts | 218 |
1 files changed, 134 insertions, 84 deletions
diff --git a/ui/src/frontend/gridline_helper.ts b/ui/src/frontend/gridline_helper.ts index 1c9dbfec8..6581c8121 100644 --- a/ui/src/frontend/gridline_helper.ts +++ b/ui/src/frontend/gridline_helper.ts @@ -13,49 +13,91 @@ // limitations under the License. import {assertTrue} from '../base/logging'; -import {roundDownNearest} from '../base/math_utils'; +import {Span, tpDurationToSeconds} from '../common/time'; +import {TPDuration, TPTime, TPTimeSpan} from '../common/time'; + import {TRACK_BORDER_COLOR, TRACK_SHELL_WIDTH} from './css_constants'; import {globals} from './globals'; import {TimeScale} from './time_scale'; -// Returns the optimal step size (in seconds) and tick pattern of ticks within -// the step. The returned step size has two properties: (1) It is 1, 2, or 5, -// multiplied by some integer power of 10. (2) It is maximised given the -// constraint: |range| / stepSize <= |maxNumberOfSteps|. -export function getStepSize( - range: number, maxNumberOfSteps: number): [number, string] { - // First, get the largest possible power of 10 that is smaller than the - // desired step size, and use it as our initial step size. - // For example, if the range is 2345ms and the desired steps is 10, then the - // minimum step size is 234.5ms so the step size will initialise to 100. - const minStepSize = range / maxNumberOfSteps; - const zeros = Math.floor(Math.log10(minStepSize)); - const initialStepSize = Math.pow(10, zeros); - - // 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. - // For our example above, this would result in a step size of 500ms, as both - // 100ms and 200ms are smaller than the minimum step size of 234.5ms. - // We pick the candidate that minimizes the step size without letting the - // number of steps exceed |maxNumberOfSteps|. The factor we pick to also - // determines the pattern of ticks. This pattern is represented using a string - // where: - // | = Major tick - // : = Medium tick - // . = Minor tick - const stepSizeMultipliers: [number, string][] = - [[1, '|....:....'], [2, '|.:.'], [5, '|....'], [10, '|....:....']]; - - for (const [multiplier, pattern] of stepSizeMultipliers) { - const newStepSize = multiplier * initialStepSize; - const numberOfNewSteps = range / newStepSize; - if (numberOfNewSteps <= maxNumberOfSteps) { - return [newStepSize, pattern]; +const micros = 1000n; +const millis = 1000n * micros; +const seconds = 1000n * millis; +const minutes = 60n * seconds; +const hours = 60n * minutes; +const days = 24n * hours; + +// These patterns cover the entire range of 0 - 2^63-1 nanoseconds +const patterns: [bigint, string][] = [ + [1n, '|'], + [2n, '|:'], + [5n, '|....'], + [10n, '|....:....'], + [20n, '|.:.'], + [50n, '|....'], + [100n, '|....:....'], + [200n, '|.:.'], + [500n, '|....'], + [1n * micros, '|....:....'], + [2n * micros, '|.:.'], + [5n * micros, '|....'], + [10n * micros, '|....:....'], + [20n * micros, '|.:.'], + [50n * micros, '|....'], + [100n * micros, '|....:....'], + [200n * micros, '|.:.'], + [500n * micros, '|....'], + [1n * millis, '|....:....'], + [2n * millis, '|.:.'], + [5n * millis, '|....'], + [10n * millis, '|....:....'], + [20n * millis, '|.:.'], + [50n * millis, '|....'], + [100n * millis, '|....:....'], + [200n * millis, '|.:.'], + [500n * millis, '|....'], + [1n * seconds, '|....:....'], + [2n * seconds, '|.:.'], + [5n * seconds, '|....'], + [10n * seconds, '|....:....'], + [30n * seconds, '|.:.:.'], + [1n * minutes, '|.....'], + [2n * minutes, '|.:.'], + [5n * minutes, '|.....'], + [10n * minutes, '|....:....'], + [30n * minutes, '|.:.:.'], + [1n * hours, '|.....'], + [2n * hours, '|.:.'], + [6n * hours, '|.....'], + [12n * hours, '|.....:.....'], + [1n * days, '|.:.'], + [2n * days, '|.:.'], + [5n * days, '|....'], + [10n * days, '|....:....'], + [20n * days, '|.:.'], + [50n * days, '|....'], + [100n * days, '|....:....'], + [200n * days, '|.:.'], + [500n * days, '|....'], + [1000n * days, '|....:....'], + [2000n * days, '|.:.'], + [5000n * days, '|....'], + [10000n * days, '|....:....'], + [20000n * days, '|.:.'], + [50000n * days, '|....'], + [100000n * days, '|....:....'], + [200000n * days, '|.:.'], +]; + +// Returns the optimal step size and pattern of ticks within the step. +export function getPattern(minPatternSize: bigint): [TPDuration, string] { + for (const [size, pattern] of patterns) { + if (size >= minPatternSize) { + return [size, pattern]; } } - throw new Error('Something has gone horribly wrong with maths'); + throw new Error('Pattern not defined for this minsize'); } function tickPatternToArray(pattern: string): TickType[] { @@ -75,21 +117,23 @@ function tickPatternToArray(pattern: string): TickType[] { }); } -// Assuming a number only has one non-zero decimal digit, find the number of -// decimal places required to accurately print that number. I.e. the parameter -// we should pass to number.toFixed(x). To account for floating point -// innaccuracies when representing numbers in base-10, we only take the first -// nonzero fractional digit into account. E.g. +// Get the number of decimal places we would have to print a time to for a given +// min step size. For example, if we know the min step size is 0.1 and all +// values are going to be aligned to integral multiples of 0.1, there's no +// point printing these values with more than 1 decimal place. +// Note: It's assumed that stepSize only has one significant figure. +// E.g. 0.3 and 0.00002 are fine, but 0.123 will be treated as if it were 0.1. +// Some examples: (seconds -> decimal places) // 1.0 -> 0 // 0.5 -> 1 // 0.009 -> 3 // 0.00007 -> 5 // 30000 -> 0 // 0.30000000000000004 -> 1 -export function guessDecimalPlaces(val: number): number { - const neglog10 = -Math.floor(Math.log10(val)); - const clamped = Math.max(0, neglog10); - return clamped; +export function guessDecimalPlaces(stepSize: TPDuration): number { + const stepSizeSeconds = tpDurationToSeconds(stepSize); + const decimalPlaces = -Math.floor(Math.log10(stepSizeSeconds)); + return Math.max(0, decimalPlaces); } export enum TickType { @@ -100,55 +144,58 @@ export enum TickType { export interface Tick { type: TickType; - time: number; - position: number; + time: TPTime; } const MIN_PX_PER_STEP = 80; +export function getMaxMajorTicks(width: number) { + return Math.max(1, Math.floor(width / MIN_PX_PER_STEP)); +} + +function roundDownNearest(time: TPTime, stepSize: TPDuration): TPTime { + return stepSize * (time / stepSize); +} // An iterable which generates a series of ticks for a given timescale. export class TickGenerator implements Iterable<Tick> { private _tickPattern: TickType[]; - private _patternSize: number; - - constructor(private scale: TimeScale, {minLabelPx = MIN_PX_PER_STEP} = {}) { - assertTrue(minLabelPx > 0, 'minLabelPx cannot be lte 0'); - assertTrue(scale.widthPx > 0, 'widthPx cannot be lte 0'); - assertTrue( - scale.timeSpan.duration > 0, 'timeSpan.duration cannot be lte 0'); - - const desiredSteps = scale.widthPx / minLabelPx; - const [size, pattern] = getStepSize(scale.timeSpan.duration, desiredSteps); + private _patternSize: TPDuration; + private _timeSpan: Span<TPTime>; + private _offset: TPTime; + + constructor( + timeSpan: Span<TPTime>, maxMajorTicks: number, offset: TPTime = 0n) { + assertTrue(timeSpan.duration > 0n, 'timeSpan.duration cannot be lte 0'); + assertTrue(maxMajorTicks > 0, 'maxMajorTicks cannot be lte 0'); + + this._timeSpan = timeSpan.add(-offset); + this._offset = offset; + const minStepSize = + BigInt(Math.floor(Number(timeSpan.duration) / maxMajorTicks)); + const [size, pattern] = getPattern(minStepSize); this._patternSize = size; this._tickPattern = tickPatternToArray(pattern); } // Returns an iterable, so this object can be iterated over directly using the // `for x of y` notation. The use of a generator here is just to make things - // more elegant than creating an array of ticks and building an iterator for - // it. + // more elegant compared to creating an array of ticks and building an + // iterator for it. * [Symbol.iterator](): Generator<Tick> { - const span = this.scale.timeSpan; - const stepSize = this._patternSize / this._tickPattern.length; - const start = roundDownNearest(span.start, this._patternSize); - const timeAtStep = (i: number) => start + (i * stepSize); - - // Iterating using steps instead of - // for (let s = start; s < span.end; s += stepSize) because if start is much - // larger than stepSize we can enter an infinite loop due to floating - // point precision errors. - for (let i = 0; timeAtStep(i) < span.end; i++) { - const time = timeAtStep(i); - if (time >= span.start) { - const position = Math.floor(this.scale.timeToPx(time)); - const type = this._tickPattern[i % this._tickPattern.length]; - yield {type, time, position}; + const stepSize = this._patternSize / BigInt(this._tickPattern.length); + const start = roundDownNearest(this._timeSpan.start, this._patternSize); + const end = this._timeSpan.end; + let patternIndex = 0; + + for (let time = start; time < end; time += stepSize, patternIndex++) { + if (time >= this._timeSpan.start) { + patternIndex = patternIndex % this._tickPattern.length; + const type = this._tickPattern[patternIndex]; + yield {type, time: time + this._offset}; } } } - // The number of decimal places labels should be printed with, assuming labels - // are only printed on major ticks. get digits(): number { return guessDecimalPlaces(this._patternSize); } @@ -157,9 +204,7 @@ export class TickGenerator implements Iterable<Tick> { // Gets the timescale associated with the current visible window. export function timeScaleForVisibleWindow( startPx: number, endPx: number): TimeScale { - const span = globals.frontendLocalState.visibleWindowTime; - const spanRelative = span.add(-globals.state.traceTime.startSec); - return new TimeScale(spanRelative, [startPx, endPx]); + return globals.frontendLocalState.getTimeScale(startPx, endPx); } export function drawGridLines( @@ -169,13 +214,18 @@ export function drawGridLines( ctx.strokeStyle = TRACK_BORDER_COLOR; ctx.lineWidth = 1; - const timeScale = timeScaleForVisibleWindow(TRACK_SHELL_WIDTH, width); - if (timeScale.timeSpan.duration > 0 && timeScale.widthPx > 0) { - for (const {type, position} of new TickGenerator(timeScale)) { + const {earliest, latest} = globals.frontendLocalState.visibleWindow; + const span = new TPTimeSpan(earliest, latest); + if (width > TRACK_SHELL_WIDTH && span.duration > 0n) { + const maxMajorTicks = getMaxMajorTicks(width - TRACK_SHELL_WIDTH); + const map = timeScaleForVisibleWindow(TRACK_SHELL_WIDTH, width); + for (const {type, time} of new TickGenerator( + span, maxMajorTicks, globals.state.traceTime.start)) { + const px = Math.floor(map.tpTimeToPx(time)); if (type === TickType.MAJOR) { ctx.beginPath(); - ctx.moveTo(position + 0.5, 0); - ctx.lineTo(position + 0.5, height); + ctx.moveTo(px + 0.5, 0); + ctx.lineTo(px + 0.5, height); ctx.stroke(); } } |