@@ -18,6 +18,11 @@ const SPLIT_NUMBER = 5;
1818// headroom out to a full tick, and leaves the rest where they are.
1919const GAP_FRACTION_OF_SPAN = 0.02 ;
2020
21+ // A percentage has a real ceiling the way zero is a real floor, so the gap must
22+ // not push the axis past it. Not every `%` sensor is bounded — power factor is
23+ // signed and can read over 100 — so this only applies while the data stays under.
24+ const PERCENT_MAX = 100 ;
25+
2126// Derive the number of decimal digits to use for Y-axis labels from the
2227// observed data range, by asking ECharts for the same tick interval it will
2328// render. This matches the precision it actually draws, so labels are neither
@@ -81,14 +86,18 @@ export function createYAxisPrecisionBounds(options: {
8186 // Such an axis also gets no gap: pushing it below zero would defeat the zero
8287 // anchoring and leave the bars floating above the axis.
8388 includeZero ?: boolean ;
89+ // Used to recognise a bounded quantity, so the gap cannot widen the axis past
90+ // a limit the data itself never crosses.
91+ unit ?: string ;
8492 onFractionDigits : ( digits : number ) => void ;
8593} ) : {
8694 min : ( values : YAxisExtentValues ) => number | undefined ;
8795 max : ( values : YAxisExtentValues ) => number | undefined ;
8896 boundaryGap : [ number , number ] ;
8997 splitNumber : number ;
9098} {
91- const { min, max, includeZero, onFractionDigits } = options ;
99+ const { min, max, includeZero, unit, onFractionDigits } = options ;
100+ const naturalMax = unit === "%" ? PERCENT_MAX : undefined ;
92101
93102 const resolveBounds = ( values : YAxisExtentValues ) => {
94103 const resolvedMin = resolveYAxisBound ( min , values ) ;
@@ -105,18 +114,36 @@ export function createYAxisPrecisionBounds(options: {
105114 resolvedMin !== undefined ,
106115 resolvedMax !== undefined ,
107116 ] ) ;
117+ // The expansion is a fraction of the magnitude, so a constant series near
118+ // the ceiling would otherwise overshoot it too.
119+ const flatMax =
120+ naturalMax !== undefined && values . max <= naturalMax
121+ ? Math . min ( flat . max , naturalMax )
122+ : flat . max ;
108123 return {
109124 min : resolvedMin ?? flat . min ,
110- max : resolvedMax ?? flat . max ,
125+ max : resolvedMax ?? flatMax ,
111126 gap : 0 ,
112127 } ;
113128 }
114129 const gap = ( values . max - values . min ) * GAP_FRACTION_OF_SPAN ;
115- // Never let the gap carry a single-signed series across zero.
130+ // Never let the gap carry a series past a boundary it does not itself cross.
131+ const floor = values . min >= 0 ? 0 : undefined ;
132+ const ceiling =
133+ naturalMax !== undefined && values . max <= naturalMax
134+ ? naturalMax
135+ : values . max <= 0
136+ ? 0
137+ : undefined ;
116138 return {
117- min : resolvedMin ?? ( values . min >= 0 && values . min < gap ? 0 : undefined ) ,
139+ min :
140+ resolvedMin ??
141+ ( floor !== undefined && values . min - floor < gap ? floor : undefined ) ,
118142 max :
119- resolvedMax ?? ( values . max <= 0 && - values . max < gap ? 0 : undefined ) ,
143+ resolvedMax ??
144+ ( ceiling !== undefined && ceiling - values . max < gap
145+ ? ceiling
146+ : undefined ) ,
120147 gap,
121148 } ;
122149 } ;
0 commit comments