Skip to content

Commit 85a5d5a

Browse files
committed
Add option to toggle user types in scatter box
1 parent 4550298 commit 85a5d5a

1 file changed

Lines changed: 129 additions & 78 deletions

File tree

src/App.tsx

Lines changed: 129 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -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.
120120
const 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+
350477
function 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

Comments
 (0)