@@ -4,7 +4,7 @@ import { UserSquare, ChevronRight, ChevronLeft, Shield } from "lucide-react";
44import { toast , Toaster } from "sonner" ;
55import {
66 LineChart , Line , XAxis , YAxis , CartesianGrid , Tooltip , Legend , ResponsiveContainer ,
7- BarChart , Bar , Cell
7+ BarChart , Bar , Cell , ScatterChart , Scatter , ZAxis
88} from "recharts" ;
99import { Card } from "@/components/ui/card" ;
1010import { Button } from "@/components/ui/button" ;
@@ -26,6 +26,7 @@ import {
2626 ProjectedUserData ,
2727 MonthOption ,
2828 UserAnalysisData ,
29+ UserBehaviorDataPoint ,
2930 aggregateDataByDay ,
3031 parseCSV ,
3132 getModelUsageSummary ,
@@ -45,6 +46,7 @@ import {
4546 getAvailableMonths ,
4647 filterDataByMonth ,
4748 getUserAnalysisData ,
49+ getUserBehaviorData ,
4850 EXCESS_REQUEST_COST
4951} from "@/lib/utils" ;
5052import { MonthSelector } from "@/components/MonthSelector" ;
@@ -58,6 +60,28 @@ const MODEL_COLORS = [
5860 "#1ABC9C" , // Turquoise
5961] ;
6062const OTHER_COLOR = "#94A3B8" ; // Slate gray for Other
63+ const BEHAVIOR_COLORS : Record < string , string > = {
64+ 'Steady Users' : '#16A34A' ,
65+ 'Low Engagement Users' : '#64748B' ,
66+ 'Burst Users' : '#DC2626' ,
67+ 'Model Explorers' : '#0EA5E9' ,
68+ 'Model Loyalists' : '#D97706' ,
69+ 'Mixed Behavior' : '#7C3AED' ,
70+ } ;
71+
72+ type BehaviorScatterPoint = UserBehaviorDataPoint & {
73+ scatterX : number ;
74+ scatterY : number ;
75+ } ;
76+
77+ function getUserJitter ( user : string ) : number {
78+ let hash = 0 ;
79+ for ( let i = 0 ; i < user . length ; i ++ ) {
80+ hash = ( hash * 31 + user . charCodeAt ( i ) ) >>> 0 ;
81+ }
82+ // Deterministic range [-0.35, +0.35]
83+ return ( ( hash % 701 ) - 350 ) / 1000 ;
84+ }
6185
6286function App ( ) {
6387 const [ showPrivacyBanner , setShowPrivacyBanner ] = useState ( true ) ;
@@ -481,6 +505,25 @@ function App() {
481505
482506 const modelBarData = useMemo ( ( ) => barChartData ( ) , [ barChartData ] ) ;
483507
508+ const behaviorData = useMemo < BehaviorScatterPoint [ ] > ( ( ) => {
509+ if ( ! displayData || ! displayData . length ) return [ ] ;
510+ return getUserBehaviorData ( displayData , selectedPlan ) . map ( ( item ) => {
511+ const jitter = getUserJitter ( item . user ) ;
512+ return {
513+ ...item ,
514+ scatterX : Math . max ( 0 , Math . min ( 100 , item . activeDaysPct + jitter ) ) ,
515+ scatterY : Math . max ( 0 , item . utilizationPct + jitter ) ,
516+ } ;
517+ } ) ;
518+ } , [ displayData , selectedPlan ] ) ;
519+
520+ const behaviorSegmentCounts = useMemo ( ( ) => {
521+ return behaviorData . reduce ( ( acc , userPoint ) => {
522+ acc [ userPoint . behaviorSegment ] = ( acc [ userPoint . behaviorSegment ] || 0 ) + 1 ;
523+ return acc ;
524+ } , { } as Record < string , number > ) ;
525+ } , [ behaviorData ] ) ;
526+
484527 const modelBarYAxis = useMemo ( ( ) => {
485528 if ( ! modelBarData . length ) {
486529 return {
@@ -1738,6 +1781,94 @@ function App() {
17381781 </ BarChart >
17391782 </ ChartContainer >
17401783 </ div >
1784+
1785+
1786+
1787+ { /* Behavior Segmentation Scatter */ }
1788+ < div className = "flex justify-between items-center mb-2 mt-8" >
1789+ < h2 className = "text-2xl font-semibold" >
1790+ User Behavior Segments
1791+ { selectedSearchUser && (
1792+ < span className = "ml-2 text-lg font-medium text-blue-600" >
1793+ - { selectedSearchUser }
1794+ </ span >
1795+ ) }
1796+ </ h2 >
1797+ < div className = "text-sm text-muted-foreground" >
1798+ X: Active Days % of Month, Y: Quota Utilization %
1799+ </ div >
1800+ </ div >
1801+ < Separator className = "mb-6" />
1802+ < div className = "bg-card p-4 rounded-lg border mb-8" >
1803+ < div className = "flex flex-wrap gap-2 mb-4" >
1804+ { Object . entries ( behaviorSegmentCounts ) . map ( ( [ segment , count ] ) => (
1805+ < div key = { segment } className = "text-xs px-2.5 py-1 rounded-full border flex items-center gap-1.5" >
1806+ < span
1807+ className = "w-2 h-2 rounded-full"
1808+ style = { { backgroundColor : BEHAVIOR_COLORS [ segment ] || '#7C3AED' } }
1809+ />
1810+ < span className = "font-medium" > { segment } :</ span >
1811+ < span > { count . toLocaleString ( ) } </ span >
1812+ </ div >
1813+ ) ) }
1814+ </ div >
1815+
1816+ < div className = "h-[460px] w-full" >
1817+ < ResponsiveContainer width = "100%" height = "100%" >
1818+ < ScatterChart margin = { { top : 16 , right : 24 , left : 8 , bottom : 8 } } >
1819+ < CartesianGrid strokeDasharray = "3 3" opacity = { 0.2 } />
1820+ < XAxis
1821+ type = "number"
1822+ dataKey = "scatterX"
1823+ name = "Active Days %"
1824+ tick = { { fill : 'var(--foreground)' } }
1825+ tickLine = { { stroke : 'var(--border)' } }
1826+ domain = { [ 0 , 100 ] }
1827+ />
1828+ < YAxis
1829+ type = "number"
1830+ dataKey = "scatterY"
1831+ name = "Utilization %"
1832+ tick = { { fill : 'var(--foreground)' } }
1833+ tickLine = { { stroke : 'var(--border)' } }
1834+ domain = { [ 0 , 'auto' ] }
1835+ />
1836+ < ZAxis type = "number" dataKey = "modelDiversity" range = { [ 80 , 420 ] } />
1837+ < Tooltip
1838+ cursor = { { strokeDasharray : '3 3' } }
1839+ content = { ( { active, payload } ) => {
1840+ if ( active && payload && payload . length ) {
1841+ const point = payload [ 0 ] . payload as BehaviorScatterPoint ;
1842+ return (
1843+ < div className = "border rounded-lg bg-background shadow-lg p-3 text-xs" >
1844+ < div className = "font-medium mb-2" > { point . user } </ div >
1845+ < div className = "space-y-1" >
1846+ < div > Segment: < span className = "font-medium" > { point . behaviorSegment } </ span > </ div >
1847+ < div > Utilization: < span className = "font-medium" > { point . utilizationPct . toLocaleString ( undefined , { maximumFractionDigits : 1 } ) } %</ span > </ div >
1848+ < div > Active Days: < span className = "font-medium" > { point . activeDaysPct . toLocaleString ( undefined , { maximumFractionDigits : 1 } ) } %</ span > </ div >
1849+ < div > Models Used: < span className = "font-medium" > { point . modelDiversity . toLocaleString ( ) } </ span > </ div >
1850+ < div > Top Model Share: < span className = "font-medium" > { point . topModelSharePct . toLocaleString ( undefined , { maximumFractionDigits : 1 } ) } %</ span > </ div >
1851+ < div > First Week Usage: < span className = "font-medium" > { ( point . frontloadIndex * 100 ) . toLocaleString ( undefined , { maximumFractionDigits : 1 } ) } %</ span > </ div >
1852+ < div > Total Requests: < span className = "font-medium" > { point . totalRequests . toLocaleString ( undefined , { maximumFractionDigits : 2 , minimumFractionDigits : 0 } ) } </ span > </ div >
1853+ </ div >
1854+ </ div >
1855+ ) ;
1856+ }
1857+ return null ;
1858+ } }
1859+ />
1860+ < Scatter name = "Users" data = { behaviorData } >
1861+ { behaviorData . map ( ( point ) => (
1862+ < Cell
1863+ key = { point . user }
1864+ fill = { BEHAVIOR_COLORS [ point . behaviorSegment ] || '#7C3AED' }
1865+ />
1866+ ) ) }
1867+ </ Scatter >
1868+ </ ScatterChart >
1869+ </ ResponsiveContainer >
1870+ </ div >
1871+ </ div >
17411872 </ div >
17421873 </ div >
17431874 ) }
0 commit comments