@@ -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+ alignSteps = false ,
73+ stepKeyword = 'step:'
7274} ) {
7375 const chartRefs = useRef ( new Map ( ) ) ;
7476 const registerChart = useCallback ( ( id , inst ) => {
@@ -103,40 +105,76 @@ export default function ChartContainer({
103105 const lines = file . content . split ( '\n' ) ;
104106 const metricsData = { } ;
105107
106- const extractByKeyword = ( content , keyword ) => {
107- const results = [ ] ;
108- const numberRegex = / [ + - ] ? \d + (?: \. \d + ) ? (?: [ e E ] [ + - ] ? \d + ) ? / ;
109- content . split ( '\n' ) . forEach ( line => {
110- const idx = line . toLowerCase ( ) . indexOf ( keyword . toLowerCase ( ) ) ;
111- if ( idx !== - 1 ) {
112- const after = line . substring ( idx + keyword . length ) ;
113- const match = after . match ( numberRegex ) ;
114- if ( match ) {
115- const v = parseFloat ( match [ 0 ] ) ;
116- if ( ! isNaN ( v ) ) results . push ( v ) ;
108+ const numberRegex = / [ + - ] ? \d + (?: \. \d + ) ? (?: [ e E ] [ + - ] ? \d + ) ? / ;
109+
110+ if ( alignSteps ) {
111+ const metricKeys = metrics . map ( ( m , idx ) => m . name || m . keyword || `metric${ idx + 1 } ` ) ;
112+ metricKeys . forEach ( k => { metricsData [ k ] = [ ] ; } ) ;
113+
114+ lines . forEach ( line => {
115+ const stepIdx = line . toLowerCase ( ) . indexOf ( stepKeyword . toLowerCase ( ) ) ;
116+ if ( stepIdx === - 1 ) return ;
117+ const afterStep = line . substring ( stepIdx + stepKeyword . length ) ;
118+ const stepMatch = afterStep . match ( / \d + / ) ;
119+ if ( ! stepMatch ) return ;
120+ const step = parseInt ( stepMatch [ 0 ] ) ;
121+ metrics . forEach ( ( metric , mi ) => {
122+ const key = metricKeys [ mi ] ;
123+ let value ;
124+ if ( metric . mode === 'keyword' ) {
125+ const idx = line . toLowerCase ( ) . indexOf ( metric . keyword . toLowerCase ( ) ) ;
126+ if ( idx !== - 1 ) {
127+ const after = line . substring ( idx + metric . keyword . length ) ;
128+ const match = after . match ( numberRegex ) ;
129+ if ( match ) value = parseFloat ( match [ 0 ] ) ;
130+ }
131+ } else if ( metric . regex ) {
132+ const reg = new RegExp ( metric . regex ) ;
133+ reg . lastIndex = 0 ;
134+ const m = reg . exec ( line ) ;
135+ if ( m && m [ 1 ] ) value = parseFloat ( m [ 1 ] ) ;
117136 }
118- }
137+ if ( value !== undefined && ! isNaN ( value ) ) {
138+ metricsData [ key ] . push ( { x : step , y : value } ) ;
139+ }
140+ } ) ;
119141 } ) ;
120- return results ;
121- } ;
122-
123- metrics . forEach ( metric => {
124- let values = [ ] ;
125- if ( metric . mode === 'keyword' ) {
126- values = extractByKeyword ( file . content , metric . keyword ) ;
127- } else if ( metric . regex ) {
128- 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 ) ;
142+ } else {
143+ const extractByKeyword = ( content , keyword ) => {
144+ const results = [ ] ;
145+ content . split ( '\n' ) . forEach ( line => {
146+ const idx = line . toLowerCase ( ) . indexOf ( keyword . toLowerCase ( ) ) ;
147+ if ( idx !== - 1 ) {
148+ const after = line . substring ( idx + keyword . length ) ;
149+ const match = after . match ( numberRegex ) ;
150+ if ( match ) {
151+ const v = parseFloat ( match [ 0 ] ) ;
152+ if ( ! isNaN ( v ) ) results . push ( v ) ;
153+ }
135154 }
136155 } ) ;
137- }
138- metricsData [ metric . name || metric . keyword ] = values . map ( ( v , i ) => ( { x : i , y : v } ) ) ;
139- } ) ;
156+ return results ;
157+ } ;
158+
159+ metrics . forEach ( ( metric , idx ) => {
160+ let values = [ ] ;
161+ if ( metric . mode === 'keyword' ) {
162+ values = extractByKeyword ( file . content , metric . keyword ) ;
163+ } else if ( metric . regex ) {
164+ const reg = new RegExp ( metric . regex ) ;
165+ lines . forEach ( line => {
166+ reg . lastIndex = 0 ;
167+ const m = reg . exec ( line ) ;
168+ if ( m && m [ 1 ] ) {
169+ const v = parseFloat ( m [ 1 ] ) ;
170+ if ( ! isNaN ( v ) ) values . push ( v ) ;
171+ }
172+ } ) ;
173+ }
174+ const key = metric . name || metric . keyword || `metric${ idx + 1 } ` ;
175+ metricsData [ key ] = values . map ( ( v , i ) => ( { x : i , y : v } ) ) ;
176+ } ) ;
177+ }
140178
141179 const range = file . config ?. dataRange ;
142180 if ( range && ( range . start > 0 || range . end !== undefined ) ) {
@@ -147,15 +185,15 @@ export default function ChartContainer({
147185 const endIndex = Math . min ( data . length , end ) ;
148186 return data . slice ( start , endIndex ) ;
149187 } ;
150- const reindex = data => data . map ( ( p , idx ) => ( { x : idx , y : p . y } ) ) ;
188+ const reindex = alignSteps ? ( data => data ) : ( data => data . map ( ( p , idx ) => ( { x : idx , y : p . y } ) ) ) ;
151189 Object . keys ( metricsData ) . forEach ( k => {
152190 metricsData [ k ] = reindex ( applyRange ( metricsData [ k ] ) ) ;
153191 } ) ;
154192 }
155193
156194 return { ...file , metricsData } ;
157195 } ) ;
158- } , [ files , metrics ] ) ;
196+ } , [ files , metrics , alignSteps , stepKeyword ] ) ;
159197
160198 useEffect ( ( ) => {
161199 const maxStep = parsedData . reduce ( ( m , f ) => {
@@ -166,15 +204,34 @@ export default function ChartContainer({
166204 } , [ parsedData , onMaxStepChange ] ) ;
167205
168206 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- } ) ;
207+ if ( alignSteps ) {
208+ const enabled = parsedData . filter ( f => f . enabled !== false ) ;
209+ if ( enabled . length > 0 ) {
210+ let minStart = Infinity ;
211+ let maxEnd = 0 ;
212+ enabled . forEach ( f => {
213+ Object . values ( f . metricsData ) . forEach ( d => {
214+ if ( d . length > 0 ) {
215+ minStart = Math . min ( minStart , d [ 0 ] . x ) ;
216+ maxEnd = Math . max ( maxEnd , d [ d . length - 1 ] . x ) ;
217+ }
218+ } ) ;
219+ } ) ;
220+ if ( minStart !== Infinity ) {
221+ onXRangeChange ( { min : minStart , max : maxEnd } ) ;
222+ }
223+ }
224+ } else {
225+ const minSteps = getMinSteps ( parsedData ) ;
226+ if ( minSteps > 0 ) {
227+ onXRangeChange ( prev => {
228+ const next = { min : 0 , max : minSteps - 1 } ;
229+ if ( prev . min === next . min && prev . max === next . max ) return prev ;
230+ return next ;
231+ } ) ;
232+ }
176233 }
177- } , [ parsedData , onXRangeChange ] ) ;
234+ } , [ parsedData , onXRangeChange , alignSteps ] ) ;
178235
179236 const colors = [ '#ef4444' , '#3b82f6' , '#10b981' , '#f59e0b' , '#8b5cf6' , '#f97316' ] ;
180237 const createChartData = dataArray => ( {
0 commit comments