@@ -40,8 +40,10 @@ const ChartWrapper = ({ data, options, chartId, onRegisterChart, onSyncHover })
4040 ...options ,
4141 onHover : ( event , activeElements ) => {
4242 if ( activeElements . length > 0 ) {
43- const step = activeElements [ 0 ] . index ;
44- onSyncHover ( step , chartId ) ;
43+ const { datasetIndex, index } = activeElements [ 0 ] ;
44+ const point = chartRef . current ?. data ?. datasets ?. [ datasetIndex ] ?. data ?. [ index ] ;
45+ const step = point ?. x ;
46+ onSyncHover ( step ?? null , chartId ) ;
4547 } else {
4648 onSyncHover ( null , chartId ) ;
4749 }
@@ -68,7 +70,9 @@ export default function ChartContainer({
6870 absoluteBaseline = 0.005 ,
6971 xRange = { min : undefined , max : undefined } ,
7072 onXRangeChange,
71- onMaxStepChange
73+ onMaxStepChange,
74+ useStepKeyword = false ,
75+ stepKeyword = 'step:'
7276} ) {
7377 const chartRefs = useRef ( new Map ( ) ) ;
7478 const registerChart = useCallback ( ( id , inst ) => {
@@ -85,8 +89,9 @@ export default function ChartContainer({
8589 } else if ( id !== sourceId ) {
8690 const activeElements = [ ] ;
8791 chart . data . datasets . forEach ( ( dataset , datasetIndex ) => {
88- if ( dataset . data && dataset . data . length > step ) {
89- activeElements . push ( { datasetIndex, index : step } ) ;
92+ const index = dataset . data . findIndex ( p => p . x === step ) ;
93+ if ( index !== - 1 ) {
94+ activeElements . push ( { datasetIndex, index } ) ;
9095 }
9196 } ) ;
9297 chart . setActiveElements ( activeElements ) ;
@@ -103,39 +108,46 @@ export default function ChartContainer({
103108 const lines = file . content . split ( '\n' ) ;
104109 const metricsData = { } ;
105110
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- }
111+ metrics . forEach ( metric => {
112+ metricsData [ metric . name || metric . keyword ] = [ ] ;
113+ } ) ;
114+
115+ const numberRegex = / [ + - ] ? \d + (?: \. \d + ) ? (?: [ e E ] [ + - ] ? \d + ) ? / ;
116+ const stepKeyLower = stepKeyword . toLowerCase ( ) ;
117+
118+ lines . forEach ( line => {
119+ let stepVal ;
120+ if ( useStepKeyword ) {
121+ const idxStep = line . toLowerCase ( ) . indexOf ( stepKeyLower ) ;
122+ if ( idxStep !== - 1 ) {
123+ const afterStep = line . substring ( idxStep + stepKeyword . length ) ;
124+ const mStep = afterStep . match ( / [ + - ] ? \d + / ) ;
125+ if ( mStep ) stepVal = parseInt ( mStep [ 0 ] ) ;
118126 }
119- } ) ;
120- return results ;
121- } ;
127+ if ( stepVal === undefined ) return ;
128+ }
122129
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+ metrics . forEach ( metric => {
131+ let value ;
132+ if ( metric . mode === 'keyword' ) {
133+ const idx = line . toLowerCase ( ) . indexOf ( metric . keyword . toLowerCase ( ) ) ;
134+ if ( idx !== - 1 ) {
135+ const after = line . substring ( idx + metric . keyword . length ) ;
136+ const m = after . match ( numberRegex ) ;
137+ if ( m ) value = parseFloat ( m [ 0 ] ) ;
138+ }
139+ } else if ( metric . regex ) {
140+ const reg = new RegExp ( metric . regex ) ;
130141 reg . lastIndex = 0 ;
131142 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- } ) ;
137- }
138- metricsData [ metric . name || metric . keyword ] = values . map ( ( v , i ) => ( { x : i , y : v } ) ) ;
143+ if ( m && m [ 1 ] ) value = parseFloat ( m [ 1 ] ) ;
144+ }
145+ if ( value !== undefined && ! isNaN ( value ) ) {
146+ const arr = metricsData [ metric . name || metric . keyword ] ;
147+ const x = useStepKeyword ? stepVal : arr . length ;
148+ arr . push ( { x, y : value } ) ;
149+ }
150+ } ) ;
139151 } ) ;
140152
141153 const range = file . config ?. dataRange ;
@@ -147,15 +159,15 @@ export default function ChartContainer({
147159 const endIndex = Math . min ( data . length , end ) ;
148160 return data . slice ( start , endIndex ) ;
149161 } ;
150- const reindex = data => data . map ( ( p , idx ) => ( { x : idx , y : p . y } ) ) ;
162+ const reindex = data => data . map ( ( p , idx ) => ( useStepKeyword ? { x : p . x , y : p . y } : { x : idx , y : p . y } ) ) ;
151163 Object . keys ( metricsData ) . forEach ( k => {
152164 metricsData [ k ] = reindex ( applyRange ( metricsData [ k ] ) ) ;
153165 } ) ;
154166 }
155167
156168 return { ...file , metricsData } ;
157169 } ) ;
158- } , [ files , metrics ] ) ;
170+ } , [ files , metrics , useStepKeyword , stepKeyword ] ) ;
159171
160172 useEffect ( ( ) => {
161173 const maxStep = parsedData . reduce ( ( m , f ) => {
@@ -166,15 +178,39 @@ export default function ChartContainer({
166178 } , [ parsedData , onMaxStepChange ] ) ;
167179
168180 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- } ) ;
181+ if ( useStepKeyword ) {
182+ const minStart = parsedData . reduce ( ( m , f ) => {
183+ const localMin = Object . values ( f . metricsData ) . reduce (
184+ ( mm , d ) => Math . min ( mm , d . length > 0 ? d [ 0 ] . x : Infinity ) ,
185+ Infinity
186+ ) ;
187+ return Math . min ( m , localMin ) ;
188+ } , Infinity ) ;
189+ const maxEnd = parsedData . reduce ( ( m , f ) => {
190+ const localMax = Object . values ( f . metricsData ) . reduce (
191+ ( mm , d ) => Math . max ( mm , d . length > 0 ? d [ d . length - 1 ] . x : - Infinity ) ,
192+ - Infinity
193+ ) ;
194+ return Math . max ( m , localMax ) ;
195+ } , - Infinity ) ;
196+ if ( minStart !== Infinity && maxEnd !== - Infinity ) {
197+ onXRangeChange ( prev => {
198+ const next = { min : minStart , max : maxEnd } ;
199+ if ( prev . min === next . min && prev . max === next . max ) return prev ;
200+ return next ;
201+ } ) ;
202+ }
203+ } else {
204+ const minSteps = getMinSteps ( parsedData ) ;
205+ if ( minSteps > 0 ) {
206+ onXRangeChange ( prev => {
207+ const next = { min : 0 , max : minSteps - 1 } ;
208+ if ( prev . min === next . min && prev . max === next . max ) return prev ;
209+ return next ;
210+ } ) ;
211+ }
176212 }
177- } , [ parsedData , onXRangeChange ] ) ;
213+ } , [ parsedData , onXRangeChange , useStepKeyword ] ) ;
178214
179215 const colors = [ '#ef4444' , '#3b82f6' , '#10b981' , '#f59e0b' , '#8b5cf6' , '#f97316' ] ;
180216 const createChartData = dataArray => ( {
0 commit comments