Skip to content

Commit 911bfda

Browse files
Vikx001Viikaax
authored andcommitted
ui: add custom range option to counter track display menu
Adds a 'Custom Range' mode to the counter track's Display submenu, letting users specify exact min/max values for the Y axis. This is useful when comparing counter tracks across different trace files, where using the same fixed range makes values directly comparable. When 'Custom Range' is selected, two numeric text inputs appear inline in the submenu for min and max. Either field can be left blank to fall back to the data-derived bound for that side. Rounding is skipped for custom ranges to honour the user-specified values. A yMax <= yMin guard prevents division-by-zero in the renderer. Fixes: #4441
1 parent c8d09cb commit 911bfda

File tree

3 files changed

+107
-5
lines changed

3 files changed

+107
-5
lines changed
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// Copyright (C) 2025 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+
15+
@import "../theme";
16+
17+
.pf-counter-track {
18+
&__custom-range {
19+
display: flex;
20+
align-items: center;
21+
gap: 4px;
22+
padding: 4px 8px;
23+
font-family: var(--pf-font-compact);
24+
font-size: var(--pf-font-size-m);
25+
26+
// Number inputs should be narrow so they fit alongside the labels.
27+
.pf-text-input {
28+
width: 64px;
29+
}
30+
}
31+
32+
&__custom-range-label {
33+
color: var(--pf-color-text-muted);
34+
white-space: nowrap;
35+
}
36+
}

ui/src/assets/perfetto.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929

3030
// Widgets/components - keep these sorted alphabetically
3131
@import "components/aggregation_adapter";
32+
@import "components/base_counter_track";
3233
@import "components/datagrid";
3334
@import "components/json_settings_editor";
3435
@import "components/pivot_table";

ui/src/components/tracks/base_counter_track.ts

Lines changed: 70 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {
3131
import {LONG, NUM} from '../../trace_processor/query_result';
3232
import {Button} from '../../widgets/button';
3333
import {MenuDivider, MenuItem, PopupMenu} from '../../widgets/menu';
34+
import {TextInput} from '../../widgets/text_input';
3435
import {checkerboardExcept} from '../checkerboard';
3536
import {valueIfAllEqual} from '../../base/array_utils';
3637
import {deferChunkedTask} from '../../base/chunked_task';
@@ -208,13 +209,20 @@ export interface CounterOptions {
208209
// zero = y-axis scale should cover the origin (zero)
209210
// minmax = y-axis scale should cover just the range of yRange
210211
// log = as minmax but also use a log scale
211-
yDisplay: 'zero' | 'minmax' | 'log';
212+
// custom = use yCustomMin/yCustomMax as the explicit bounds
213+
yDisplay: 'zero' | 'minmax' | 'log' | 'custom';
212214

213215
// Whether the range boundaries should be strict and use the precise min/max
214216
// values or whether they should be rounded down/up to the nearest human
215217
// readable value.
216218
yRangeRounding: 'strict' | 'human_readable';
217219

220+
// When yDisplay is 'custom', use these exact values as the y-axis min/max
221+
// instead of deriving them from the data. Either can be undefined to fall
222+
// back to the data-derived bound.
223+
yCustomMin?: number;
224+
yCustomMax?: number;
225+
218226
// Scales the height of the chart.
219227
chartHeightSize: ChartHeightSize;
220228

@@ -248,7 +256,7 @@ type yMode = z.infer<typeof ymodeSchema>;
248256
const yRangeSchema = z.union([z.literal('all'), z.literal('viewport')]);
249257
type YRange = z.infer<typeof yRangeSchema>;
250258

251-
const yDisplaySchema = z.enum(['zero', 'minmax', 'log']);
259+
const yDisplaySchema = z.enum(['zero', 'minmax', 'log', 'custom']);
252260
type YDisplay = z.infer<typeof yDisplaySchema>;
253261

254262
const yRangeRoundingSchema = z.union([
@@ -335,7 +343,7 @@ const yRangeSettingDescriptor: TrackSettingDescriptor<YRange> = {
335343
const yDisplaySettingDescriptor: TrackSettingDescriptor<YDisplay> = {
336344
id: 'yDisplay',
337345
name: 'Y-axis display',
338-
description: 'zero, minmax, log',
346+
description: 'zero, minmax, log, custom',
339347
schema: yDisplaySchema,
340348
defaultValue: 'zero',
341349
render(setter, values) {
@@ -356,6 +364,11 @@ const yDisplaySettingDescriptor: TrackSettingDescriptor<YDisplay> = {
356364
onclick: () => setter('log'),
357365
icon: value === 'log' ? radioIconChecked : radioIconUnchecked,
358366
}),
367+
m(MenuItem, {
368+
label: 'Custom Range',
369+
onclick: () => setter('custom'),
370+
icon: value === 'custom' ? radioIconChecked : radioIconUnchecked,
371+
}),
359372
]);
360373
},
361374
};
@@ -581,8 +594,46 @@ export abstract class BaseCounterTrack implements TrackRenderer {
581594
this.invalidate();
582595
},
583596
}),
597+
598+
m(MenuItem, {
599+
label: 'Custom Range',
600+
icon:
601+
options.yDisplay === 'custom'
602+
? 'radio_button_checked'
603+
: 'radio_button_unchecked',
604+
onclick: () => {
605+
options.yDisplay = 'custom';
606+
this.invalidate();
607+
},
608+
}),
584609
),
585610

611+
options.yDisplay === 'custom' &&
612+
m('.pf-counter-track__custom-range', [
613+
m('span.pf-counter-track__custom-range-label', 'Min'),
614+
m(TextInput, {
615+
type: 'number',
616+
placeholder: 'auto',
617+
value: options.yCustomMin ?? '',
618+
onInput: (v) => {
619+
const parsed = parseFloat(v);
620+
options.yCustomMin = isNaN(parsed) ? undefined : parsed;
621+
this.invalidate();
622+
},
623+
}),
624+
m('span.pf-counter-track__custom-range-label', 'Max'),
625+
m(TextInput, {
626+
type: 'number',
627+
placeholder: 'auto',
628+
value: options.yCustomMax ?? '',
629+
onInput: (v) => {
630+
const parsed = parseFloat(v);
631+
options.yCustomMax = isNaN(parsed) ? undefined : parsed;
632+
this.invalidate();
633+
},
634+
}),
635+
]),
636+
586637
m(
587638
MenuItem,
588639
{
@@ -1080,6 +1131,11 @@ export abstract class BaseCounterTrack implements TrackRenderer {
10801131
yMax = Math.max(0, yMax);
10811132
}
10821133

1134+
if (options.yDisplay === 'custom') {
1135+
if (options.yCustomMin !== undefined) yMin = options.yCustomMin;
1136+
if (options.yCustomMax !== undefined) yMax = options.yCustomMax;
1137+
}
1138+
10831139
if (options.yOverrideMaximum !== undefined) {
10841140
yMax = Math.max(options.yOverrideMaximum, yMax);
10851141
}
@@ -1088,7 +1144,11 @@ export abstract class BaseCounterTrack implements TrackRenderer {
10881144
yMin = Math.min(options.yOverrideMinimum, yMin);
10891145
}
10901146

1091-
if (options.yRangeRounding === 'human_readable') {
1147+
// Skip rounding when the user has specified an exact custom range.
1148+
if (
1149+
options.yRangeRounding === 'human_readable' &&
1150+
options.yDisplay !== 'custom'
1151+
) {
10921152
if (options.yDisplay === 'log') {
10931153
yMax = Math.log(roundAway(Math.exp(yMax)));
10941154
yMin = Math.log(roundAway(Math.exp(yMin)));
@@ -1098,11 +1158,16 @@ export abstract class BaseCounterTrack implements TrackRenderer {
10981158
}
10991159
}
11001160

1161+
// Ensure yMax > yMin to prevent division by zero in the renderer.
1162+
if (yMax <= yMin) {
1163+
yMax = yMin + 1;
1164+
}
1165+
11011166
[yMin, yMax] = this.rangeSharer.share(options, [yMin, yMax]);
11021167

11031168
let yLabel: string;
11041169

1105-
if (options.yDisplay === 'minmax') {
1170+
if (options.yDisplay === 'minmax' || options.yDisplay === 'custom') {
11061171
yLabel = 'min - max';
11071172
} else {
11081173
let max = yMax;

0 commit comments

Comments
 (0)