Skip to content

Commit df96310

Browse files
committed
#develop show uncategorized
1 parent 711242d commit df96310

2 files changed

Lines changed: 100 additions & 21 deletions

File tree

apps/desktop/src/routes/window.activity.overview/components/ActivityBalanceChart.tsx

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export const ActivityBalanceChart: React.FC<TActivityBalanceChartProps> = (props
1919
const bins = React.useMemo(() => computeBins(activities, startOfDay), [activities, startOfDay]);
2020

2121
const maxPositive = React.useMemo(
22-
() => Math.max(1, ...bins.map((b) => b.focused + b.neutral)),
22+
() => Math.max(1, ...bins.map((b) => b.focused + b.neutral + b.uncategorized)),
2323
[bins]
2424
);
2525
const maxNegative = React.useMemo(() => Math.max(1, ...bins.map((b) => b.distracting)), [bins]);
@@ -84,21 +84,48 @@ export const ActivityBalanceChart: React.FC<TActivityBalanceChartProps> = (props
8484
bin.focused > 0 ? Math.max((bin.focused / maxPositive) * halfH, minBarPx) : 0;
8585
const neutralH =
8686
bin.neutral > 0 ? Math.max((bin.neutral / maxPositive) * halfH, minBarPx) : 0;
87+
const uncategorizedH =
88+
bin.uncategorized > 0
89+
? Math.max((bin.uncategorized / maxPositive) * halfH, minBarPx)
90+
: 0;
8791
const distractingH =
8892
bin.distracting > 0
8993
? Math.max((bin.distracting / maxNegative) * halfH, minBarPx)
9094
: 0;
9195

9296
const focusedActivities = bin.activities.filter((a) => a.category === 'focused');
9397
const neutralActivities = bin.activities.filter((a) => a.category === 'neutral');
98+
const uncategorizedActivities = bin.activities.filter((a) =>
99+
isUncategorizedActivity(a.category)
100+
);
94101
const distractingActivities = bin.activities.filter(
95102
(a) => a.category === 'distracting'
96103
);
97104

98105
return (
99106
<div key={h} className="relative flex flex-1 flex-col">
100-
{/* Upper half — focused (bottom) + neutral (top), touching axis */}
107+
{/* Upper half — focused (bottom), neutral, uncategorized (top), touching axis */}
101108
<div className="flex flex-col justify-end" style={{ height: halfH }}>
109+
{uncategorizedH > 0 && (
110+
<Tooltip
111+
content={
112+
<FocusCategoryTooltip
113+
category={null}
114+
durationMs={bin.uncategorized}
115+
activities={uncategorizedActivities}
116+
/>
117+
}
118+
side="bottom"
119+
>
120+
<div
121+
className="w-full cursor-default hover:opacity-80"
122+
style={{
123+
height: uncategorizedH,
124+
backgroundColor: categoryToColor(null)
125+
}}
126+
/>
127+
</Tooltip>
128+
)}
102129
{neutralH > 0 && (
103130
<Tooltip
104131
content={
@@ -196,6 +223,7 @@ function computeBins(activities: specta.WindowActivityDto[], startOfDay: number)
196223
const bins: THourBin[] = Array.from({ length: 24 }, () => ({
197224
focused: 0,
198225
neutral: 0,
226+
uncategorized: 0,
199227
distracting: 0,
200228
activities: []
201229
}));
@@ -215,6 +243,7 @@ function computeBins(activities: specta.WindowActivityDto[], startOfDay: number)
215243
if (cat === 'focused') bin.focused += overlap;
216244
else if (cat === 'neutral') bin.neutral += overlap;
217245
else if (cat === 'distracting') bin.distracting += overlap;
246+
else bin.uncategorized += overlap;
218247

219248
bin.activities.push({ ...activity, startedAt: clippedStart, endedAt: clippedEnd });
220249
}
@@ -226,11 +255,16 @@ function computeBins(activities: specta.WindowActivityDto[], startOfDay: number)
226255
interface THourBin {
227256
focused: number; // ms
228257
neutral: number; // ms
258+
uncategorized: number; // ms
229259
distracting: number; // ms
230260
/** Activities clipped to this hour's boundaries */
231261
activities: specta.WindowActivityDto[];
232262
}
233263

264+
function isUncategorizedActivity(category: specta.WindowActivityDto['category']): boolean {
265+
return category !== 'focused' && category !== 'neutral' && category !== 'distracting';
266+
}
267+
234268
function niceTickInterval(maxMs: number, maxTicks = 3): number {
235269
const intervals = [60_000, 120_000, 300_000, 600_000, 900_000, 1_800_000, 3_600_000];
236270
return intervals.find((i) => Math.ceil(maxMs / i) <= maxTicks) ?? 3_600_000;

apps/desktop/src/routes/window.activity.overview/components/FocusPulse.tsx

Lines changed: 64 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,44 @@
1-
import { Tooltip, TooltipProvider } from '@repo/ui';
1+
import { formatDuration, Tooltip, TooltipProvider } from '@repo/ui';
22
import React from 'react';
33
import type { specta } from '@/environment';
44
import { categoryToColor, FocusCategoryTooltip } from '@/features/focus';
55

66
export const FocusPulse: React.FC<TFocusPulseProps> = (props) => {
77
const { activities } = props;
88

9-
const { focusedMs, neutralMs, distractingMs, score } = React.useMemo(() => {
10-
let focusedMs = 0;
11-
let neutralMs = 0;
12-
let distractingMs = 0;
13-
for (const a of activities) {
14-
const dur = a.endedAt - a.startedAt;
15-
if (a.category === 'focused') focusedMs += dur;
16-
else if (a.category === 'neutral') neutralMs += dur;
17-
else if (a.category === 'distracting') distractingMs += dur;
18-
}
19-
const totalMs = focusedMs + neutralMs + distractingMs;
20-
const score =
21-
totalMs === 0
22-
? 0
23-
: Math.min(100, Math.round(((focusedMs + neutralMs * 0.5) / totalMs) * 100));
24-
return { focusedMs, neutralMs, distractingMs, score };
25-
}, [activities]);
9+
const { focusedMs, neutralMs, distractingMs, uncategorizedMs, categorizedMs, score } =
10+
React.useMemo(() => {
11+
let focusedMs = 0;
12+
let neutralMs = 0;
13+
let distractingMs = 0;
14+
let uncategorizedMs = 0;
15+
for (const a of activities) {
16+
const dur = a.endedAt - a.startedAt;
17+
if (a.category === 'focused') focusedMs += dur;
18+
else if (a.category === 'neutral') neutralMs += dur;
19+
else if (a.category === 'distracting') distractingMs += dur;
20+
else uncategorizedMs += dur;
21+
}
22+
const categorizedMs = focusedMs + neutralMs + distractingMs;
23+
const score =
24+
categorizedMs === 0
25+
? 0
26+
: Math.min(100, Math.round(((focusedMs + neutralMs * 0.5) / categorizedMs) * 100));
27+
return { focusedMs, neutralMs, distractingMs, uncategorizedMs, categorizedMs, score };
28+
}, [activities]);
2629

2730
const size = 128;
2831
const center = size / 2;
2932
const r = 52;
3033
const strokeWidth = 14;
3134
const c = 2 * Math.PI * r;
32-
const totalMs = focusedMs + neutralMs + distractingMs;
35+
const totalMs = categorizedMs + uncategorizedMs;
3336

3437
const focusedLen = totalMs > 0 ? (focusedMs / totalMs) * c : 0;
3538
const neutralLen = totalMs > 0 ? (neutralMs / totalMs) * c : 0;
3639
const distractingLen = totalMs > 0 ? (distractingMs / totalMs) * c : 0;
40+
const uncategorizedLen = totalMs > 0 ? (uncategorizedMs / totalMs) * c : 0;
41+
const categorizedPct = totalMs > 0 ? Math.round((categorizedMs / totalMs) * 100) : 0;
3742

3843
const focusedActivities = React.useMemo(
3944
() => activities.filter((activity) => activity.category === 'focused'),
@@ -47,6 +52,10 @@ export const FocusPulse: React.FC<TFocusPulseProps> = (props) => {
4752
() => activities.filter((activity) => activity.category === 'distracting'),
4853
[activities]
4954
);
55+
const uncategorizedActivities = React.useMemo(
56+
() => activities.filter((activity) => isUncategorizedActivity(activity.category)),
57+
[activities]
58+
);
5059

5160
// MARK: - UI
5261

@@ -147,6 +156,33 @@ export const FocusPulse: React.FC<TFocusPulseProps> = (props) => {
147156
/>
148157
</Tooltip>
149158
)}
159+
{/* Uncategorized arc */}
160+
{uncategorizedLen > 0 && (
161+
<Tooltip
162+
content={
163+
<FocusCategoryTooltip
164+
category={null}
165+
durationMs={uncategorizedMs}
166+
activities={uncategorizedActivities}
167+
/>
168+
}
169+
side="left"
170+
positionerClassName="z-50"
171+
>
172+
<circle
173+
cx={center}
174+
cy={center}
175+
r={r}
176+
fill="none"
177+
strokeWidth={strokeWidth}
178+
stroke={categoryToColor(null)}
179+
strokeDasharray={`${uncategorizedLen} ${c}`}
180+
strokeDashoffset={-(focusedLen + neutralLen + distractingLen)}
181+
transform={`rotate(-90 ${center} ${center})`}
182+
className="cursor-default transition-opacity hover:opacity-80"
183+
/>
184+
</Tooltip>
185+
)}
150186
</svg>
151187
{/* Score */}
152188
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
@@ -156,6 +192,11 @@ export const FocusPulse: React.FC<TFocusPulseProps> = (props) => {
156192

157193
{/* Label */}
158194
<span className="text-base-400 text-[11px]">focus score</span>
195+
<span className="text-base-400 text-[11px]">
196+
{uncategorizedMs > 0
197+
? `${categorizedPct}% categorized · ${formatDuration(uncategorizedMs / 1000)} uncategorized`
198+
: 'fully categorized'}
199+
</span>
159200
</div>
160201
</TooltipProvider>
161202
);
@@ -164,3 +205,7 @@ export const FocusPulse: React.FC<TFocusPulseProps> = (props) => {
164205
interface TFocusPulseProps {
165206
activities: specta.WindowActivityDto[];
166207
}
208+
209+
function isUncategorizedActivity(category: specta.WindowActivityDto['category']): boolean {
210+
return category !== 'focused' && category !== 'neutral' && category !== 'distracting';
211+
}

0 commit comments

Comments
 (0)