@@ -116,7 +116,7 @@ type WeeklyTopModelsChartProps = {
116116} ;
117117
118118// Isolated the graph due to latency issues.
119- // Was updating selected week on clicked buttons, causing all graphs to re-render.
119+ // Updates selected week on clicked buttons, causing all graphs to re-render.
120120const WeeklyTopModelsChart = React . memo ( function WeeklyTopModelsChart ( {
121121 dailyModelData,
122122 top5Models,
@@ -347,6 +347,133 @@ const WeeklyTopModelsChart = React.memo(function WeeklyTopModelsChart({
347347 ) ;
348348} ) ;
349349
350+ type BehaviorScatterChartProps = {
351+ behaviorData : BehaviorScatterPoint [ ] ;
352+ } ;
353+
354+ const BehaviorScatterChart = React . memo ( function BehaviorScatterChart ( {
355+ behaviorData,
356+ } : BehaviorScatterChartProps ) {
357+ const [ hiddenBehaviorSegments , setHiddenBehaviorSegments ] = useState < Set < string > > ( new Set ( ) ) ;
358+
359+ const behaviorSegmentCounts = useMemo ( ( ) => {
360+ return behaviorData . reduce ( ( acc , userPoint ) => {
361+ acc [ userPoint . behaviorSegment ] = ( acc [ userPoint . behaviorSegment ] || 0 ) + 1 ;
362+ return acc ;
363+ } , { } as Record < string , number > ) ;
364+ } , [ behaviorData ] ) ;
365+
366+ const dataBySegment = useMemo ( ( ) => {
367+ const result : Record < string , BehaviorScatterPoint [ ] > = { } ;
368+ for ( const point of behaviorData ) {
369+ if ( ! result [ point . behaviorSegment ] ) result [ point . behaviorSegment ] = [ ] ;
370+ result [ point . behaviorSegment ] . push ( point ) ;
371+ }
372+ return result ;
373+ } , [ behaviorData ] ) ;
374+
375+ const yAxisMax = useMemo ( ( ) => {
376+ if ( ! behaviorData . length ) return 100 ;
377+ return getNiceAxisCeiling ( Math . max ( ...behaviorData . map ( p => p . scatterY ) ) ) ;
378+ } , [ behaviorData ] ) ;
379+
380+ const toggleBehaviorSegment = useCallback ( ( segment : string ) => {
381+ setHiddenBehaviorSegments ( prev => {
382+ const next = new Set ( prev ) ;
383+ if ( next . has ( segment ) ) {
384+ next . delete ( segment ) ;
385+ } else {
386+ next . add ( segment ) ;
387+ }
388+ return next ;
389+ } ) ;
390+ } , [ ] ) ;
391+
392+ return (
393+ < div className = "bg-card p-4 rounded-lg border mb-8" >
394+ < div className = "flex flex-wrap gap-2 mb-4" >
395+ { Object . entries ( behaviorSegmentCounts ) . map ( ( [ segment , count ] ) => {
396+ const isHidden = hiddenBehaviorSegments . has ( segment ) ;
397+ return (
398+ < button
399+ key = { segment }
400+ onClick = { ( ) => toggleBehaviorSegment ( segment ) }
401+ className = { `text-xs px-2.5 py-1 rounded-full border flex items-center gap-1.5 cursor-pointer transition-opacity select-none ${
402+ isHidden ? 'opacity-40' : 'opacity-100'
403+ } `}
404+ title = { isHidden ? `Show ${ segment } ` : `Hide ${ segment } ` }
405+ >
406+ < span
407+ className = "w-2 h-2 rounded-full"
408+ style = { { backgroundColor : isHidden ? '#94A3B8' : ( BEHAVIOR_COLORS [ segment ] || '#7C3AED' ) } }
409+ />
410+ < span className = { `font-medium ${ isHidden ? 'line-through' : '' } ` } > { segment } :</ span >
411+ < span > { count . toLocaleString ( ) } </ span >
412+ </ button >
413+ ) ;
414+ } ) }
415+ </ div >
416+
417+ < div className = "h-[460px] w-full" >
418+ < ResponsiveContainer width = "100%" height = "100%" >
419+ < ScatterChart margin = { { top : 16 , right : 24 , left : 8 , bottom : 8 } } >
420+ < CartesianGrid strokeDasharray = "3 3" opacity = { 0.2 } />
421+ < XAxis
422+ type = "number"
423+ dataKey = "scatterX"
424+ name = "Active Days %"
425+ tick = { { fill : 'var(--foreground)' } }
426+ tickLine = { { stroke : 'var(--border)' } }
427+ domain = { [ 0 , 100 ] }
428+ />
429+ < YAxis
430+ type = "number"
431+ dataKey = "scatterY"
432+ name = "Utilization %"
433+ tick = { { fill : 'var(--foreground)' } }
434+ tickLine = { { stroke : 'var(--border)' } }
435+ domain = { [ 0 , yAxisMax ] }
436+ />
437+ < ZAxis type = "number" dataKey = "modelDiversity" range = { [ 80 , 420 ] } />
438+ < Tooltip
439+ cursor = { { strokeDasharray : '3 3' } }
440+ content = { ( { active, payload } ) => {
441+ if ( active && payload && payload . length ) {
442+ const point = payload [ 0 ] . payload as BehaviorScatterPoint ;
443+ return (
444+ < div className = "border rounded-lg bg-background shadow-lg p-3 text-xs" >
445+ < div className = "font-medium mb-2" > { point . user } </ div >
446+ < div className = "space-y-1" >
447+ < div > Segment: < span className = "font-medium" > { point . behaviorSegment } </ span > </ div >
448+ < div > Utilization: < span className = "font-medium" > { point . utilizationPct . toLocaleString ( undefined , { maximumFractionDigits : 1 } ) } %</ span > </ div >
449+ < div > Active Days: < span className = "font-medium" > { point . activeDaysPct . toLocaleString ( undefined , { maximumFractionDigits : 1 } ) } %</ span > </ div >
450+ < div > Models Used: < span className = "font-medium" > { point . modelDiversity . toLocaleString ( ) } </ span > </ div >
451+ < div > Top Model Share: < span className = "font-medium" > { point . topModelSharePct . toLocaleString ( undefined , { maximumFractionDigits : 1 } ) } %</ span > </ div >
452+ < div > First Week Usage: < span className = "font-medium" > { ( point . frontloadIndex * 100 ) . toLocaleString ( undefined , { maximumFractionDigits : 1 } ) } %</ span > </ div >
453+ < div > Total Requests: < span className = "font-medium" > { point . totalRequests . toLocaleString ( undefined , { maximumFractionDigits : 2 , minimumFractionDigits : 0 } ) } </ span > </ div >
454+ </ div >
455+ </ div >
456+ ) ;
457+ }
458+ return null ;
459+ } }
460+ />
461+ { Object . entries ( dataBySegment ) . map ( ( [ segment , points ] ) => (
462+ < Scatter
463+ key = { segment }
464+ name = { segment }
465+ data = { hiddenBehaviorSegments . has ( segment ) ? [ ] : points }
466+ fill = { BEHAVIOR_COLORS [ segment ] || '#7C3AED' }
467+ isAnimationActive = { false }
468+ />
469+ ) ) }
470+ </ ScatterChart >
471+ </ ResponsiveContainer >
472+ </ div >
473+ </ div >
474+ ) ;
475+ } ) ;
476+
350477function App ( ) {
351478 const [ showPrivacyBanner , setShowPrivacyBanner ] = useState ( true ) ;
352479 const [ data , setData ] = useState < CopilotUsageData [ ] | null > ( null ) ;
@@ -751,13 +878,6 @@ function App() {
751878 } ) ;
752879 } , [ displayData , selectedPlan ] ) ;
753880
754- const behaviorSegmentCounts = useMemo ( ( ) => {
755- return behaviorData . reduce ( ( acc , userPoint ) => {
756- acc [ userPoint . behaviorSegment ] = ( acc [ userPoint . behaviorSegment ] || 0 ) + 1 ;
757- return acc ;
758- } , { } as Record < string , number > ) ;
759- } , [ behaviorData ] ) ;
760-
761881 // Get all unique models from the data (not just top 5), sorted by total requests (descending)
762882 const getAllUniqueModels = useCallback ( ( ) => {
763883 if ( ! dailyModelData . length ) return [ ] ;
@@ -1944,76 +2064,7 @@ function App() {
19442064 </ div >
19452065 </ div >
19462066 < Separator className = "mb-6" />
1947- < div className = "bg-card p-4 rounded-lg border mb-8" >
1948- < div className = "flex flex-wrap gap-2 mb-4" >
1949- { Object . entries ( behaviorSegmentCounts ) . map ( ( [ segment , count ] ) => (
1950- < div key = { segment } className = "text-xs px-2.5 py-1 rounded-full border flex items-center gap-1.5" >
1951- < span
1952- className = "w-2 h-2 rounded-full"
1953- style = { { backgroundColor : BEHAVIOR_COLORS [ segment ] || '#7C3AED' } }
1954- />
1955- < span className = "font-medium" > { segment } :</ span >
1956- < span > { count . toLocaleString ( ) } </ span >
1957- </ div >
1958- ) ) }
1959- </ div >
1960-
1961- < div className = "h-[460px] w-full" >
1962- < ResponsiveContainer width = "100%" height = "100%" >
1963- < ScatterChart margin = { { top : 16 , right : 24 , left : 8 , bottom : 8 } } >
1964- < CartesianGrid strokeDasharray = "3 3" opacity = { 0.2 } />
1965- < XAxis
1966- type = "number"
1967- dataKey = "scatterX"
1968- name = "Active Days %"
1969- tick = { { fill : 'var(--foreground)' } }
1970- tickLine = { { stroke : 'var(--border)' } }
1971- domain = { [ 0 , 100 ] }
1972- />
1973- < YAxis
1974- type = "number"
1975- dataKey = "scatterY"
1976- name = "Utilization %"
1977- tick = { { fill : 'var(--foreground)' } }
1978- tickLine = { { stroke : 'var(--border)' } }
1979- domain = { [ 0 , 'auto' ] }
1980- />
1981- < ZAxis type = "number" dataKey = "modelDiversity" range = { [ 80 , 420 ] } />
1982- < Tooltip
1983- cursor = { { strokeDasharray : '3 3' } }
1984- content = { ( { active, payload } ) => {
1985- if ( active && payload && payload . length ) {
1986- const point = payload [ 0 ] . payload as BehaviorScatterPoint ;
1987- return (
1988- < div className = "border rounded-lg bg-background shadow-lg p-3 text-xs" >
1989- < div className = "font-medium mb-2" > { point . user } </ div >
1990- < div className = "space-y-1" >
1991- < div > Segment: < span className = "font-medium" > { point . behaviorSegment } </ span > </ div >
1992- < div > Utilization: < span className = "font-medium" > { point . utilizationPct . toLocaleString ( undefined , { maximumFractionDigits : 1 } ) } %</ span > </ div >
1993- < div > Active Days: < span className = "font-medium" > { point . activeDaysPct . toLocaleString ( undefined , { maximumFractionDigits : 1 } ) } %</ span > </ div >
1994- < div > Models Used: < span className = "font-medium" > { point . modelDiversity . toLocaleString ( ) } </ span > </ div >
1995- < div > Top Model Share: < span className = "font-medium" > { point . topModelSharePct . toLocaleString ( undefined , { maximumFractionDigits : 1 } ) } %</ span > </ div >
1996- < div > First Week Usage: < span className = "font-medium" > { ( point . frontloadIndex * 100 ) . toLocaleString ( undefined , { maximumFractionDigits : 1 } ) } %</ span > </ div >
1997- < div > Total Requests: < span className = "font-medium" > { point . totalRequests . toLocaleString ( undefined , { maximumFractionDigits : 2 , minimumFractionDigits : 0 } ) } </ span > </ div >
1998- </ div >
1999- </ div >
2000- ) ;
2001- }
2002- return null ;
2003- } }
2004- />
2005- < Scatter name = "Users" data = { behaviorData } >
2006- { behaviorData . map ( ( point ) => (
2007- < Cell
2008- key = { point . user }
2009- fill = { BEHAVIOR_COLORS [ point . behaviorSegment ] || '#7C3AED' }
2010- />
2011- ) ) }
2012- </ Scatter >
2013- </ ScatterChart >
2014- </ ResponsiveContainer >
2015- </ div >
2016- </ div >
2067+ < BehaviorScatterChart behaviorData = { behaviorData } />
20172068 </ div >
20182069 </ div >
20192070 ) }
0 commit comments