@@ -40,7 +40,8 @@ const ChartWrapper = ({ data, options, chartId, onRegisterChart, onSyncHover })
4040 ...options ,
4141 onHover : ( event , activeElements ) => {
4242 if ( activeElements . length > 0 ) {
43- const step = activeElements [ 0 ] . index ;
43+ const el = activeElements [ 0 ] . element ;
44+ const step = el ?. $context ?. parsed ?. x ?? activeElements [ 0 ] . index ;
4445 onSyncHover ( step , chartId ) ;
4546 } else {
4647 onSyncHover ( null , chartId ) ;
@@ -68,7 +69,9 @@ export default function ChartContainer({
6869 absoluteBaseline = 0.005 ,
6970 xRange = { min : undefined , max : undefined } ,
7071 onXRangeChange,
71- onMaxStepChange
72+ onMaxStepChange,
73+ stepKeyword = 'step:' ,
74+ useStepKeyword = false
7275} ) {
7376 const chartRefs = useRef ( new Map ( ) ) ;
7477 const registerChart = useCallback ( ( id , inst ) => {
@@ -85,8 +88,9 @@ export default function ChartContainer({
8588 } else if ( id !== sourceId ) {
8689 const activeElements = [ ] ;
8790 chart . data . datasets . forEach ( ( dataset , datasetIndex ) => {
88- if ( dataset . data && dataset . data . length > step ) {
89- activeElements . push ( { datasetIndex, index : step } ) ;
91+ const idx = dataset . data . findIndex ( p => p . x === step ) ;
92+ if ( idx !== - 1 ) {
93+ activeElements . push ( { datasetIndex, index : idx } ) ;
9094 }
9195 } ) ;
9296 chart . setActiveElements ( activeElements ) ;
@@ -100,62 +104,73 @@ export default function ChartContainer({
100104 const enabled = files . filter ( f => f . enabled !== false ) ;
101105 return enabled . map ( file => {
102106 if ( ! file . content ) return { ...file , metricsData : { } } ;
107+
103108 const lines = file . content . split ( '\n' ) ;
104109 const metricsData = { } ;
110+ metrics . forEach ( metric => {
111+ metricsData [ metric . name || metric . keyword ] = [ ] ;
112+ } ) ;
105113
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 ) ;
117- }
118- }
119- } ) ;
120- return results ;
121- } ;
114+ const escapeRegex = s => s . replace ( / [ . * + ? ^ $ { } ( ) | [ \] \\ ] / g, '\\$&' ) ;
115+ const stepReg = useStepKeyword && stepKeyword
116+ ? new RegExp ( `${ escapeRegex ( stepKeyword ) } \\s*\\[?\\s*(\\d+)` , 'i' )
117+ : null ;
118+ const numberRegex = / [ + - ] ? \d + (?: \. \d + ) ? (?: [ e E ] [ + - ] ? \d + ) ? / ;
122119
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 ;
120+ lines . forEach ( line => {
121+ const stepMatch = stepReg ? stepReg . exec ( line ) : null ;
122+ const stepVal = stepMatch ? parseInt ( stepMatch [ 1 ] ) : null ;
123+
124+ metrics . forEach ( metric => {
125+ let value ;
126+ if ( metric . mode === 'keyword' && metric . keyword ) {
127+ const idx = line . toLowerCase ( ) . indexOf ( metric . keyword . toLowerCase ( ) ) ;
128+ if ( idx !== - 1 ) {
129+ const after = line . substring ( idx + metric . keyword . length ) ;
130+ const match = after . match ( numberRegex ) ;
131+ if ( match ) {
132+ const v = parseFloat ( match [ 0 ] ) ;
133+ if ( ! isNaN ( v ) ) value = v ;
134+ }
135+ }
136+ } else if ( metric . regex ) {
137+ const reg = new RegExp ( metric . regex ) ;
131138 const m = reg . exec ( line ) ;
132139 if ( m && m [ 1 ] ) {
133140 const v = parseFloat ( m [ 1 ] ) ;
134- if ( ! isNaN ( v ) ) values . push ( v ) ;
141+ if ( ! isNaN ( v ) ) value = v ;
135142 }
136- } ) ;
137- }
138- metricsData [ metric . name || metric . keyword ] = values . map ( ( v , i ) => ( { x : i , y : v } ) ) ;
143+ }
144+
145+ if ( value !== undefined ) {
146+ const arr = metricsData [ metric . name || metric . keyword ] ;
147+ if ( useStepKeyword ) {
148+ if ( stepVal !== null ) arr . push ( { x : stepVal , y : value } ) ;
149+ } else {
150+ arr . push ( { x : arr . length , y : value } ) ;
151+ }
152+ }
153+ } ) ;
139154 } ) ;
140155
141156 const range = file . config ?. dataRange ;
142157 if ( range && ( range . start > 0 || range . end !== undefined ) ) {
143- const applyRange = data => {
144- if ( data . length === 0 ) return data ;
158+ Object . keys ( metricsData ) . forEach ( k => {
159+ const data = metricsData [ k ] ;
160+ if ( data . length === 0 ) return ;
145161 const start = Math . max ( 0 , parseInt ( range . start ) || 0 ) ;
146162 const end = range . end !== undefined ? parseInt ( range . end ) : data . length ;
147163 const endIndex = Math . min ( data . length , end ) ;
148- return data . slice ( start , endIndex ) ;
149- } ;
150- const reindex = data => data . map ( ( p , idx ) => ( { x : idx , y : p . y } ) ) ;
151- Object . keys ( metricsData ) . forEach ( k => {
152- metricsData [ k ] = reindex ( applyRange ( metricsData [ k ] ) ) ;
164+ const sliced = data . slice ( start , endIndex ) ;
165+ metricsData [ k ] = useStepKeyword
166+ ? sliced
167+ : sliced . map ( ( p , idx ) => ( { x : idx , y : p . y } ) ) ;
153168 } ) ;
154169 }
155170
156171 return { ...file , metricsData } ;
157172 } ) ;
158- } , [ files , metrics ] ) ;
173+ } , [ files , metrics , stepKeyword , useStepKeyword ] ) ;
159174
160175 useEffect ( ( ) => {
161176 const maxStep = parsedData . reduce ( ( m , f ) => {
@@ -166,15 +181,33 @@ export default function ChartContainer({
166181 } , [ parsedData , onMaxStepChange ] ) ;
167182
168183 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 ;
184+ if ( useStepKeyword ) {
185+ const ranges = [ ] ;
186+ parsedData . forEach ( f => {
187+ Object . values ( f . metricsData ) . forEach ( d => {
188+ if ( d . length > 0 ) ranges . push ( { min : d [ 0 ] . x , max : d [ d . length - 1 ] . x } ) ;
189+ } ) ;
175190 } ) ;
191+ if ( ranges . length > 0 ) {
192+ const globalMin = Math . min ( ...ranges . map ( r => r . min ) ) ;
193+ const globalMax = Math . max ( ...ranges . map ( r => r . max ) ) ;
194+ onXRangeChange ( prev => {
195+ const next = { min : globalMin , max : globalMax } ;
196+ if ( prev . min === next . min && prev . max === next . max ) return prev ;
197+ return next ;
198+ } ) ;
199+ }
200+ } else {
201+ const minSteps = getMinSteps ( parsedData ) ;
202+ if ( minSteps > 0 ) {
203+ onXRangeChange ( prev => {
204+ const next = { min : 0 , max : minSteps - 1 } ;
205+ if ( prev . min === next . min && prev . max === next . max ) return prev ;
206+ return next ;
207+ } ) ;
208+ }
176209 }
177- } , [ parsedData , onXRangeChange ] ) ;
210+ } , [ parsedData , onXRangeChange , useStepKeyword ] ) ;
178211
179212 const colors = [ '#ef4444' , '#3b82f6' , '#10b981' , '#f59e0b' , '#8b5cf6' , '#f97316' ] ;
180213 const createChartData = dataArray => ( {
0 commit comments