@@ -31,6 +31,7 @@ import {
3131import { LONG , NUM } from '../../trace_processor/query_result' ;
3232import { Button } from '../../widgets/button' ;
3333import { MenuDivider , MenuItem , PopupMenu } from '../../widgets/menu' ;
34+ import { TextInput } from '../../widgets/text_input' ;
3435import { checkerboardExcept } from '../checkerboard' ;
3536import { valueIfAllEqual } from '../../base/array_utils' ;
3637import { 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>;
248256const yRangeSchema = z . union ( [ z . literal ( 'all' ) , z . literal ( 'viewport' ) ] ) ;
249257type YRange = z . infer < typeof yRangeSchema > ;
250258
251- const yDisplaySchema = z . enum ( [ 'zero' , 'minmax' , 'log' ] ) ;
259+ const yDisplaySchema = z . enum ( [ 'zero' , 'minmax' , 'log' , 'custom' ] ) ;
252260type YDisplay = z . infer < typeof yDisplaySchema > ;
253261
254262const yRangeRoundingSchema = z . union ( [
@@ -335,7 +343,7 @@ const yRangeSettingDescriptor: TrackSettingDescriptor<YRange> = {
335343const 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