@@ -12,6 +12,9 @@ import {
1212 Tooltip ,
1313 Legend ,
1414 ResponsiveContainer ,
15+ ReferenceLine ,
16+ ComposedChart ,
17+ Line ,
1518} from 'recharts' ;
1619import './Graph.css' ;
1720
@@ -169,9 +172,100 @@ const Graph: React.FC<GraphProps> = ({ config, data }) => {
169172 const metricX = metrics [ 0 ] ;
170173 const metricY = metrics [ 1 ] ;
171174
175+ const groupBy = config . groupBy || 'tokenizer' ;
176+
177+ const getLanguageInfoForLabel = ( label : string ) : any => {
178+ const parts = label . split ( '_' ) ;
179+ const base = parts . length >= 3 ? parts . slice ( 0 , parts . length - 2 ) . join ( '_' ) : label ;
180+ const glottocode = parts . length >= 3 ? parts [ parts . length - 1 ] : undefined ;
181+ const root : any = data ?. metadata ?. languagesInfo || { } ;
182+ const direct = root . languages && root . languages [ base ] ? root . languages [ base ]
183+ : ( root [ base ] ? root [ base ] : null ) ;
184+ if ( direct ) return direct ;
185+ if ( glottocode ) {
186+ const entries : Array < { key : string ; info : any } > = root . languages && typeof root . languages === 'object'
187+ ? Object . keys ( root . languages ) . map ( ( k ) => ( { key : k , info : root . languages [ k ] } ) )
188+ : Object . keys ( root ) . map ( ( k ) => ( { key : k , info : root [ k ] } ) ) ;
189+ for ( const { info } of entries ) {
190+ const gc = info ?. glottocodes ;
191+ if ( typeof gc === 'string' && gc === glottocode ) return info ;
192+ if ( Array . isArray ( gc ) && gc . includes ( glottocode ) ) return info ;
193+ if ( gc && typeof gc === 'object' && Object . keys ( gc ) . includes ( glottocode ) ) return info ;
194+ }
195+ }
196+ return null ;
197+ } ;
198+
199+ const getFamilyForLanguage = ( label : string ) : string => {
200+ const info = getLanguageInfoForLabel ( label ) ;
201+ if ( ! info ) return 'unknown' ;
202+ const fam = info . families ;
203+ if ( Array . isArray ( fam ) ) return fam [ 0 ] || 'unknown' ;
204+ if ( fam && typeof fam === 'object' ) {
205+ const keys = Object . keys ( fam ) ;
206+ return keys [ 0 ] || 'unknown' ;
207+ }
208+ if ( typeof fam === 'string' ) return fam || 'unknown' ;
209+ return 'unknown' ;
210+ } ;
211+
212+ const groupKeyForPoint = ( pt : any ) : string => {
213+ if ( groupBy === 'tokenizer' ) return pt . tokenizer || 'unknown' ;
214+ if ( groupBy === 'language' ) return pt . language || 'unknown' ;
215+ if ( groupBy === 'family' ) return getFamilyForLanguage ( pt . language || '' ) ;
216+ return 'unknown' ;
217+ } ;
218+
219+ // Partition chartData into groups
220+ const groupsMap : Map < string , any [ ] > = new Map ( ) ;
221+ ( Array . isArray ( chartData ) ? chartData : [ ] ) . forEach ( ( pt ) => {
222+ const key = groupKeyForPoint ( pt ) ;
223+ if ( ! groupsMap . has ( key ) ) groupsMap . set ( key , [ ] ) ;
224+ groupsMap . get ( key ) ! . push ( pt ) ;
225+ } ) ;
226+ const groupNames = Array . from ( groupsMap . keys ( ) ) ;
227+
228+ const allPoints : any [ ] = ( Array . isArray ( chartData ) ? chartData : [ ] ) . filter ( ( pt ) => {
229+ const x = pt [ metricX ] ;
230+ const y = pt [ metricY ] ;
231+ return typeof x === 'number' && typeof y === 'number' && ! Number . isNaN ( x ) && ! Number . isNaN ( y ) && isFinite ( x ) && isFinite ( y ) ;
232+ } ) ;
233+
234+ const computeTrend = ( pts : any [ ] ) : { m : number ; b : number ; minX : number ; maxX : number } | null => {
235+ if ( ! pts || pts . length < 2 ) return null ;
236+ let sumX = 0 , sumY = 0 , sumXY = 0 , sumXX = 0 ;
237+ let minX = Infinity , maxX = - Infinity ;
238+ for ( const p of pts ) {
239+ const x = p [ metricX ] ;
240+ const y = p [ metricY ] ;
241+ sumX += x ;
242+ sumY += y ;
243+ sumXY += x * y ;
244+ sumXX += x * x ;
245+ if ( x < minX ) minX = x ;
246+ if ( x > maxX ) maxX = x ;
247+ }
248+ const n = pts . length ;
249+ const denom = ( n * sumXX - sumX * sumX ) ;
250+ if ( denom === 0 ) return null ;
251+ const m = ( n * sumXY - sumX * sumY ) / denom ;
252+ const b = ( sumY - m * sumX ) / n ;
253+ return { m, b, minX, maxX } ;
254+ } ;
255+
256+ const trend = config . showTrendline ? computeTrend ( allPoints ) : null ;
257+
258+ // Use ComposedChart to overlay a line on scatter
259+ const trendData = trend
260+ ? [
261+ { [ metricX ] : trend . minX , [ metricY ] : trend . m * trend . minX + trend . b } ,
262+ { [ metricX ] : trend . maxX , [ metricY ] : trend . m * trend . maxX + trend . b } ,
263+ ]
264+ : [ ] ;
265+
172266 return (
173- < ResponsiveContainer width = "100%" height = { 400 } >
174- < ScatterChart margin = { { top : 20 , right : 30 , left : 60 , bottom : 60 } } >
267+ < ResponsiveContainer width = "100%" height = { 420 } >
268+ < ComposedChart margin = { { top : 20 , right : 30 , left : 60 , bottom : 90 } } >
175269 < CartesianGrid strokeDasharray = "3 3" />
176270 < XAxis
177271 type = "number"
@@ -202,13 +296,29 @@ const Graph: React.FC<GraphProps> = ({ config, data }) => {
202296 return null ;
203297 } }
204298 />
205- < Scatter
206- name = "Metric Pair Correlation"
207- data = { chartData }
208- fill = "#8884d8"
209- isAnimationActive = { false }
210- />
211- </ ScatterChart >
299+ < Legend verticalAlign = "bottom" align = "center" wrapperStyle = { { paddingTop : 10 } } />
300+ { trend && (
301+ < Line
302+ type = "linear"
303+ data = { trendData }
304+ dataKey = { metricY }
305+ name = "Trend"
306+ stroke = "#444"
307+ strokeDasharray = "4 2"
308+ dot = { false }
309+ isAnimationActive = { false }
310+ />
311+ ) }
312+ { groupNames . map ( ( name , idx ) => (
313+ < Scatter
314+ key = { name }
315+ name = { name }
316+ data = { groupsMap . get ( name ) ! }
317+ fill = { getColorForMetric ( idx ) }
318+ isAnimationActive = { false }
319+ />
320+ ) ) }
321+ </ ComposedChart >
212322 </ ResponsiveContainer >
213323 ) ;
214324 } ;
0 commit comments