@@ -68,7 +68,9 @@ export default function ChartContainer({
6868 absoluteBaseline = 0.005 ,
6969 xRange = { min : undefined , max : undefined } ,
7070 onXRangeChange,
71- onMaxStepChange
71+ onMaxStepChange,
72+ useStepKeyword = false ,
73+ stepKeyword = 'step:'
7274} ) {
7375 const chartRefs = useRef ( new Map ( ) ) ;
7476 const registerChart = useCallback ( ( id , inst ) => {
@@ -103,17 +105,60 @@ export default function ChartContainer({
103105 const lines = file . content . split ( '\n' ) ;
104106 const metricsData = { } ;
105107
106- const extractByKeyword = ( content , keyword ) => {
108+ const numberRegex = / [ + - ] ? \d + (?: \. \d + ) ? (?: [ e E ] [ + - ] ? \d + ) ? / ;
109+
110+ const extractByKeyword = ( keyword ) => {
107111 const results = [ ] ;
108- const numberRegex = / [ + - ] ? \d + (?: \. \d + ) ? (?: [ e E ] [ + - ] ? \d + ) ? / ;
109- content . split ( '\n' ) . forEach ( line => {
110- const idx = line . toLowerCase ( ) . indexOf ( keyword . toLowerCase ( ) ) ;
112+ const lowerMetric = keyword . toLowerCase ( ) ;
113+ lines . forEach ( line => {
114+ const idx = line . toLowerCase ( ) . indexOf ( lowerMetric ) ;
111115 if ( idx !== - 1 ) {
112116 const after = line . substring ( idx + keyword . length ) ;
113117 const match = after . match ( numberRegex ) ;
114118 if ( match ) {
115119 const v = parseFloat ( match [ 0 ] ) ;
116- if ( ! isNaN ( v ) ) results . push ( v ) ;
120+ if ( ! isNaN ( v ) ) {
121+ let x = results . length ;
122+ if ( useStepKeyword ) {
123+ const stepIdx = line . toLowerCase ( ) . indexOf ( stepKeyword . toLowerCase ( ) ) ;
124+ if ( stepIdx !== - 1 ) {
125+ const stepAfter = line . substring ( stepIdx + stepKeyword . length ) ;
126+ const stepMatch = stepAfter . match ( numberRegex ) ;
127+ if ( stepMatch ) {
128+ const s = parseFloat ( stepMatch [ 0 ] ) ;
129+ if ( ! isNaN ( s ) ) x = s ;
130+ }
131+ }
132+ }
133+ results . push ( { x, y : v } ) ;
134+ }
135+ }
136+ }
137+ } ) ;
138+ return results ;
139+ } ;
140+
141+ const extractByRegex = ( reg ) => {
142+ const results = [ ] ;
143+ lines . forEach ( line => {
144+ reg . lastIndex = 0 ;
145+ const m = reg . exec ( line ) ;
146+ if ( m && m [ 1 ] ) {
147+ const v = parseFloat ( m [ 1 ] ) ;
148+ if ( ! isNaN ( v ) ) {
149+ let x = results . length ;
150+ if ( useStepKeyword ) {
151+ const stepIdx = line . toLowerCase ( ) . indexOf ( stepKeyword . toLowerCase ( ) ) ;
152+ if ( stepIdx !== - 1 ) {
153+ const stepAfter = line . substring ( stepIdx + stepKeyword . length ) ;
154+ const stepMatch = stepAfter . match ( numberRegex ) ;
155+ if ( stepMatch ) {
156+ const s = parseFloat ( stepMatch [ 0 ] ) ;
157+ if ( ! isNaN ( s ) ) x = s ;
158+ }
159+ }
160+ }
161+ results . push ( { x, y : v } ) ;
117162 }
118163 }
119164 } ) ;
@@ -123,39 +168,38 @@ export default function ChartContainer({
123168 metrics . forEach ( metric => {
124169 let values = [ ] ;
125170 if ( metric . mode === 'keyword' ) {
126- values = extractByKeyword ( file . content , metric . keyword ) ;
171+ values = extractByKeyword ( metric . keyword ) ;
127172 } else if ( metric . regex ) {
128173 const reg = new RegExp ( metric . regex ) ;
129- lines . forEach ( line => {
130- reg . lastIndex = 0 ;
131- const m = reg . exec ( line ) ;
132- if ( m && m [ 1 ] ) {
133- const v = parseFloat ( m [ 1 ] ) ;
134- if ( ! isNaN ( v ) ) values . push ( v ) ;
135- }
136- } ) ;
174+ values = extractByRegex ( reg ) ;
137175 }
138- metricsData [ metric . name || metric . keyword ] = values . map ( ( v , i ) => ( { x : i , y : v } ) ) ;
176+ metricsData [ metric . name || metric . keyword ] = values ;
139177 } ) ;
140178
141179 const range = file . config ?. dataRange ;
142180 if ( range && ( range . start > 0 || range . end !== undefined ) ) {
143181 const applyRange = data => {
144182 if ( data . length === 0 ) return data ;
145- const start = Math . max ( 0 , parseInt ( range . start ) || 0 ) ;
146- const end = range . end !== undefined ? parseInt ( range . end ) : data . length ;
147- const endIndex = Math . min ( data . length , end ) ;
148- return data . slice ( start , endIndex ) ;
183+ if ( useStepKeyword ) {
184+ const start = parseInt ( range . start ) || 0 ;
185+ const end = range . end !== undefined ? parseInt ( range . end ) : Infinity ;
186+ return data . filter ( p => p . x >= start && p . x <= end ) ;
187+ } else {
188+ const start = Math . max ( 0 , parseInt ( range . start ) || 0 ) ;
189+ const end = range . end !== undefined ? parseInt ( range . end ) : data . length ;
190+ const endIndex = Math . min ( data . length , end ) ;
191+ const sliced = data . slice ( start , endIndex ) ;
192+ return sliced . map ( ( p , idx ) => ( { x : idx , y : p . y } ) ) ;
193+ }
149194 } ;
150- const reindex = data => data . map ( ( p , idx ) => ( { x : idx , y : p . y } ) ) ;
151195 Object . keys ( metricsData ) . forEach ( k => {
152- metricsData [ k ] = reindex ( applyRange ( metricsData [ k ] ) ) ;
196+ metricsData [ k ] = applyRange ( metricsData [ k ] ) ;
153197 } ) ;
154198 }
155199
156200 return { ...file , metricsData } ;
157201 } ) ;
158- } , [ files , metrics ] ) ;
202+ } , [ files , metrics , useStepKeyword , stepKeyword ] ) ;
159203
160204 useEffect ( ( ) => {
161205 const maxStep = parsedData . reduce ( ( m , f ) => {
@@ -166,15 +210,43 @@ export default function ChartContainer({
166210 } , [ parsedData , onMaxStepChange ] ) ;
167211
168212 useEffect ( ( ) => {
169- const minSteps = getMinSteps ( parsedData ) ;
170- if ( minSteps > 0 ) {
171- onXRangeChange ( prev => {
172- const next = { min : 0 , max : minSteps - 1 } ;
173- if ( prev . min === next . min && prev . max === next . max ) return prev ;
174- return next ;
175- } ) ;
213+ if ( useStepKeyword ) {
214+ const ranges = parsedData . map ( f => {
215+ const datasets = Object . values ( f . metricsData ) ;
216+ if ( datasets . length === 0 ) return null ;
217+ let start = - Infinity ;
218+ let end = Infinity ;
219+ datasets . forEach ( d => {
220+ if ( d . length > 0 ) {
221+ if ( d [ 0 ] . x > start ) start = d [ 0 ] . x ;
222+ if ( d [ d . length - 1 ] . x < end ) end = d [ d . length - 1 ] . x ;
223+ }
224+ } ) ;
225+ if ( start === - Infinity || end === Infinity ) return null ;
226+ return { start, end } ;
227+ } ) . filter ( Boolean ) ;
228+ if ( ranges . length > 0 ) {
229+ const start = Math . max ( ...ranges . map ( r => r . start ) ) ;
230+ const end = Math . min ( ...ranges . map ( r => r . end ) ) ;
231+ if ( end >= start ) {
232+ onXRangeChange ( prev => {
233+ const next = { min : start , max : end } ;
234+ if ( prev . min === next . min && prev . max === next . max ) return prev ;
235+ return next ;
236+ } ) ;
237+ }
238+ }
239+ } else {
240+ const minSteps = getMinSteps ( parsedData ) ;
241+ if ( minSteps > 0 ) {
242+ onXRangeChange ( prev => {
243+ const next = { min : 0 , max : minSteps - 1 } ;
244+ if ( prev . min === next . min && prev . max === next . max ) return prev ;
245+ return next ;
246+ } ) ;
247+ }
176248 }
177- } , [ parsedData , onXRangeChange ] ) ;
249+ } , [ parsedData , onXRangeChange , useStepKeyword ] ) ;
178250
179251 const colors = [ '#ef4444' , '#3b82f6' , '#10b981' , '#f59e0b' , '#8b5cf6' , '#f97316' ] ;
180252 const createChartData = dataArray => ( {
0 commit comments