Skip to content

Commit 9838368

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 9838368

File tree

3 files changed

+110
-4
lines changed

3 files changed

+110
-4
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: 73 additions & 4 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,12 @@ 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+
closePopupOnClick: false,
372+
}),
359373
]);
360374
},
361375
};
@@ -581,8 +595,47 @@ export abstract class BaseCounterTrack implements TrackRenderer {
581595
this.invalidate();
582596
},
583597
}),
598+
599+
m(MenuItem, {
600+
label: 'Custom Range',
601+
icon:
602+
options.yDisplay === 'custom'
603+
? 'radio_button_checked'
604+
: 'radio_button_unchecked',
605+
closePopupOnClick: false,
606+
onclick: () => {
607+
options.yDisplay = 'custom';
608+
this.invalidate();
609+
},
610+
}),
584611
),
585612

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

1136+
if (options.yDisplay === 'custom') {
1137+
if (options.yCustomMin !== undefined) yMin = options.yCustomMin;
1138+
if (options.yCustomMax !== undefined) yMax = options.yCustomMax;
1139+
}
1140+
10831141
if (options.yOverrideMaximum !== undefined) {
10841142
yMax = Math.max(options.yOverrideMaximum, yMax);
10851143
}
@@ -1088,7 +1146,11 @@ export abstract class BaseCounterTrack implements TrackRenderer {
10881146
yMin = Math.min(options.yOverrideMinimum, yMin);
10891147
}
10901148

1091-
if (options.yRangeRounding === 'human_readable') {
1149+
// Skip rounding when the user has specified an exact custom range.
1150+
if (
1151+
options.yRangeRounding === 'human_readable' &&
1152+
options.yDisplay !== 'custom'
1153+
) {
10921154
if (options.yDisplay === 'log') {
10931155
yMax = Math.log(roundAway(Math.exp(yMax)));
10941156
yMin = Math.log(roundAway(Math.exp(yMin)));
@@ -1098,12 +1160,19 @@ export abstract class BaseCounterTrack implements TrackRenderer {
10981160
}
10991161
}
11001162

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

11031170
let yLabel: string;
11041171

11051172
if (options.yDisplay === 'minmax') {
11061173
yLabel = 'min - max';
1174+
} else if (options.yDisplay === 'custom') {
1175+
yLabel = 'custom';
11071176
} else {
11081177
let max = yMax;
11091178
let min = yMin;

0 commit comments

Comments
 (0)