docs(09): create phase plan for weight classification and visualization
This commit is contained in:
@@ -0,0 +1,309 @@
|
||||
---
|
||||
phase: 09-weight-classification-and-visualization
|
||||
plan: 02
|
||||
type: execute
|
||||
wave: 2
|
||||
depends_on: ["09-01"]
|
||||
files_modified:
|
||||
- src/client/components/WeightSummaryCard.tsx
|
||||
- src/client/routes/setups/$setupId.tsx
|
||||
- package.json
|
||||
autonomous: false
|
||||
requirements: [CLAS-02, VIZZ-01, VIZZ-02, VIZZ-03]
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "Setup detail view shows separate weight subtotals for base weight, worn weight, consumable weight, and total"
|
||||
- "User can view a donut chart showing weight distribution by category in the setup"
|
||||
- "User can toggle the chart between category breakdown and classification breakdown via pill toggle"
|
||||
- "Hovering a chart segment shows category/classification name, weight in selected unit, and percentage"
|
||||
- "Total weight displayed in the center of the donut hole"
|
||||
artifacts:
|
||||
- path: "src/client/components/WeightSummaryCard.tsx"
|
||||
provides: "Summary card with weight subtotals, donut chart, pill toggle, and tooltips"
|
||||
min_lines: 100
|
||||
- path: "src/client/routes/setups/$setupId.tsx"
|
||||
provides: "WeightSummaryCard rendered below sticky bar when setup has items"
|
||||
- path: "package.json"
|
||||
provides: "recharts dependency installed"
|
||||
contains: "recharts"
|
||||
key_links:
|
||||
- from: "src/client/components/WeightSummaryCard.tsx"
|
||||
to: "recharts"
|
||||
via: "PieChart, Pie, Cell, Tooltip, Label, ResponsiveContainer imports"
|
||||
pattern: "from.*recharts"
|
||||
- from: "src/client/components/WeightSummaryCard.tsx"
|
||||
to: "src/client/lib/formatters.ts"
|
||||
via: "formatWeight for subtotals and tooltip display"
|
||||
pattern: "formatWeight"
|
||||
- from: "src/client/routes/setups/$setupId.tsx"
|
||||
to: "src/client/components/WeightSummaryCard.tsx"
|
||||
via: "WeightSummaryCard rendered with setup.items prop"
|
||||
pattern: "WeightSummaryCard"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Add the WeightSummaryCard component with classification weight subtotals, a donut chart for weight distribution, and a pill toggle for switching between category and classification views.
|
||||
|
||||
Purpose: Users need to visualize how weight is distributed across their setup -- both by gear category (shelter, sleep, cook) and by classification (base weight, worn, consumable). The donut chart with tooltips makes weight analysis intuitive.
|
||||
|
||||
Output: A summary card below the setup sticky bar showing Base | Worn | Consumable | Total weight columns alongside a donut chart with interactive tooltips, togglable between category and classification breakdowns.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@/home/jlmak/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@/home/jlmak/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/09-weight-classification-and-visualization/09-CONTEXT.md
|
||||
@.planning/phases/09-weight-classification-and-visualization/09-RESEARCH.md
|
||||
@.planning/phases/09-weight-classification-and-visualization/09-01-SUMMARY.md
|
||||
|
||||
<interfaces>
|
||||
<!-- Key types and contracts from Plan 01. Executor uses these directly. -->
|
||||
|
||||
From src/client/hooks/useSetups.ts (after Plan 01):
|
||||
```typescript
|
||||
interface SetupItemWithCategory {
|
||||
id: number;
|
||||
name: string;
|
||||
weightGrams: number | null;
|
||||
priceCents: number | null;
|
||||
categoryId: number;
|
||||
notes: string | null;
|
||||
productUrl: string | null;
|
||||
imageFilename: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
categoryName: string;
|
||||
categoryIcon: string;
|
||||
classification: string; // "base" | "worn" | "consumable" -- added by Plan 01
|
||||
}
|
||||
```
|
||||
|
||||
From src/client/lib/formatters.ts:
|
||||
```typescript
|
||||
export type WeightUnit = "g" | "oz" | "lb" | "kg";
|
||||
export function formatWeight(grams: number | null | undefined, unit: WeightUnit = "g"): string;
|
||||
```
|
||||
|
||||
From src/client/hooks/useWeightUnit.ts:
|
||||
```typescript
|
||||
export function useWeightUnit(): WeightUnit;
|
||||
```
|
||||
|
||||
From 09-RESEARCH.md (Recharts pattern):
|
||||
```typescript
|
||||
import { PieChart, Pie, Cell, Tooltip, Label, ResponsiveContainer } from "recharts";
|
||||
// Use Cell for per-slice colors (still functional in v3, deprecated for v4)
|
||||
// Use fixed numeric height on ResponsiveContainer (e.g., height={200})
|
||||
// Filter out zero-weight entries before passing to chart
|
||||
```
|
||||
|
||||
From 09-RESEARCH.md (color palettes):
|
||||
```typescript
|
||||
const CATEGORY_COLORS = [
|
||||
"#6366f1", "#f59e0b", "#10b981", "#ef4444", "#8b5cf6",
|
||||
"#06b6d4", "#f97316", "#ec4899", "#14b8a6", "#84cc16",
|
||||
];
|
||||
const CLASSIFICATION_COLORS = {
|
||||
base: "#6366f1", // indigo
|
||||
worn: "#f59e0b", // amber
|
||||
consumable: "#10b981", // emerald
|
||||
};
|
||||
```
|
||||
|
||||
From 09-CONTEXT.md (locked decisions):
|
||||
- Summary card below sticky bar, always visible when setup has items
|
||||
- Card with columns layout: Base | Worn | Consumable | Total
|
||||
- Donut chart inside the summary card alongside weight subtotals
|
||||
- Pill toggle above the chart: "Category" / "Classification" (same style as weight unit selector)
|
||||
- Total weight in center of donut hole
|
||||
- Hover tooltips: segment name, weight in selected unit, percentage
|
||||
- Chart library: Recharts (PieChart + Pie with innerRadius)
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Install Recharts, create WeightSummaryCard, wire into setup detail page</name>
|
||||
<files>
|
||||
src/client/components/WeightSummaryCard.tsx,
|
||||
src/client/routes/setups/$setupId.tsx,
|
||||
package.json
|
||||
</files>
|
||||
<action>
|
||||
1. **Install Recharts**: Run `bun add recharts`. This adds recharts to package.json. React and react-dom are already peer deps in the project.
|
||||
|
||||
2. **Create WeightSummaryCard component** (`src/client/components/WeightSummaryCard.tsx`):
|
||||
|
||||
**Props interface:**
|
||||
```typescript
|
||||
interface WeightSummaryCardProps {
|
||||
items: SetupItemWithCategory[]; // from useSetups hook (includes classification field)
|
||||
}
|
||||
```
|
||||
Import `SetupItemWithCategory` from `../hooks/useSetups`.
|
||||
|
||||
**State:** `viewMode: "category" | "classification"` -- local React state, default "category".
|
||||
|
||||
**Weight subtotals computation** (derive from items array):
|
||||
```typescript
|
||||
const baseWeight = items.reduce((sum, i) => i.classification === "base" ? sum + (i.weightGrams ?? 0) : sum, 0);
|
||||
const wornWeight = items.reduce((sum, i) => i.classification === "worn" ? sum + (i.weightGrams ?? 0) : sum, 0);
|
||||
const consumableWeight = items.reduce((sum, i) => i.classification === "consumable" ? sum + (i.weightGrams ?? 0) : sum, 0);
|
||||
const totalWeight = baseWeight + wornWeight + consumableWeight;
|
||||
```
|
||||
|
||||
**Chart data transformation:**
|
||||
- `buildCategoryChartData(items)`: Group by `categoryName`, sum `weightGrams`, compute percentage. Filter out zero-weight groups. Return `Array<{ name: string, weight: number, percent: number }>`.
|
||||
- `buildClassificationChartData(items)`: Group by classification using labels ("Base Weight", "Worn", "Consumable"), sum weights, compute percentage. Filter out zero-weight groups.
|
||||
- Select data source based on `viewMode`.
|
||||
|
||||
**Render structure:**
|
||||
```
|
||||
<div className="bg-white rounded-xl border border-gray-100 p-5 mb-6">
|
||||
<!-- Pill toggle: Category | Classification -->
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-sm font-medium text-gray-700">Weight Summary</h3>
|
||||
<PillToggle viewMode={viewMode} onChange={setViewMode} />
|
||||
</div>
|
||||
|
||||
<!-- Main content: chart + subtotals side by side -->
|
||||
<div className="flex items-center gap-8">
|
||||
<!-- Donut chart -->
|
||||
<div className="flex-shrink-0" style={{ width: 180, height: 180 }}>
|
||||
<ResponsiveContainer width="100%" height={180}>
|
||||
<PieChart>
|
||||
<Pie data={chartData} dataKey="weight" nameKey="name"
|
||||
cx="50%" cy="50%" innerRadius={55} outerRadius={80} paddingAngle={2}>
|
||||
{chartData.map((entry, index) => (
|
||||
<Cell key={entry.name} fill={colors[index % colors.length]} />
|
||||
))}
|
||||
<Label value={formatWeight(totalWeight, unit)} position="center"
|
||||
style={{ fontSize: "14px", fontWeight: 600, fill: "#374151" }} />
|
||||
</Pie>
|
||||
<Tooltip content={<CustomTooltip unit={unit} />} />
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
|
||||
<!-- Weight subtotals columns -->
|
||||
<div className="flex-1 grid grid-cols-4 gap-4">
|
||||
<SubtotalColumn label="Base" weight={baseWeight} unit={unit} color="#6366f1" />
|
||||
<SubtotalColumn label="Worn" weight={wornWeight} unit={unit} color="#f59e0b" />
|
||||
<SubtotalColumn label="Consumable" weight={consumableWeight} unit={unit} color="#10b981" />
|
||||
<SubtotalColumn label="Total" weight={totalWeight} unit={unit} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Pill toggle** (inline component or extracted):
|
||||
- Two buttons in a `bg-gray-100 rounded-full` container: "Category" and "Classification".
|
||||
- Active state: `bg-white text-gray-700 shadow-sm font-medium`. Inactive: `text-gray-400 hover:text-gray-600`.
|
||||
- Same pattern as TotalsBar weight unit selector.
|
||||
|
||||
**SubtotalColumn** (inline component):
|
||||
- Vertical stack: colored dot (if color provided), label in text-xs text-gray-500, weight value in text-sm font-semibold text-gray-900.
|
||||
|
||||
**CustomTooltip:**
|
||||
- Props: `active`, `payload`, `unit` (WeightUnit).
|
||||
- When active and payload exists, show: segment name (bold), weight formatted with `formatWeight()`, percentage as `(XX.X%)`.
|
||||
- Styled: `bg-white border border-gray-200 rounded-lg shadow-lg px-3 py-2 text-sm`.
|
||||
|
||||
**Color selection:**
|
||||
- When `viewMode === "category"`: use `CATEGORY_COLORS` array (cycle through for many categories).
|
||||
- When `viewMode === "classification"`: use `CLASSIFICATION_COLORS` object (keyed by classification value).
|
||||
|
||||
**Edge cases:**
|
||||
- If all items have null/zero weight, show a placeholder message ("No weight data to display") instead of the chart.
|
||||
- If items array is empty, component should not render (handled by parent).
|
||||
|
||||
3. **Wire into setup detail page** (`src/client/routes/setups/$setupId.tsx`):
|
||||
- Import `WeightSummaryCard` from `../../components/WeightSummaryCard`.
|
||||
- Render `<WeightSummaryCard items={setup.items} />` between the actions bar and the items grid (before the `{itemCount > 0 && (` block), but INSIDE the `itemCount > 0` condition so it only shows when there are items.
|
||||
- Exact placement: after the actions `<div>` and before the items-grouped-by-category `<div>`, within the `{itemCount > 0 && (...)}` block.
|
||||
|
||||
4. **Verify**: Run `bun run build` to ensure no TypeScript errors and Recharts imports resolve correctly.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>bun run build</automated>
|
||||
</verify>
|
||||
<done>
|
||||
- WeightSummaryCard renders below sticky bar when setup has items
|
||||
- Shows 4 columns: Base | Worn | Consumable | Total with correct weight values in selected unit
|
||||
- Donut chart renders with colored segments for weight distribution
|
||||
- Pill toggle switches between category view and classification view
|
||||
- Hovering chart segments shows tooltip with name, weight, and percentage
|
||||
- Total weight displayed in center of donut hole
|
||||
- Empty/zero-weight items handled gracefully
|
||||
- Build succeeds with no TypeScript errors
|
||||
</done>
|
||||
</task>
|
||||
|
||||
<task type="checkpoint:human-verify" gate="blocking">
|
||||
<name>Task 2: Visual verification of complete weight classification and visualization</name>
|
||||
<files>N/A</files>
|
||||
<action>
|
||||
Present the user with verification steps for the complete Phase 9 feature set.
|
||||
This checkpoint covers both Plan 01 (classification badges) and Plan 02 (summary card + chart) together.
|
||||
</action>
|
||||
<what-built>
|
||||
Complete weight classification and visualization system:
|
||||
1. Classification badges on every item card in setup view (click to cycle: base weight / worn / consumable)
|
||||
2. Weight summary card with Base | Worn | Consumable | Total subtotals
|
||||
3. Donut chart with category/classification toggle and hover tooltips
|
||||
4. Total weight in the center of the donut hole
|
||||
</what-built>
|
||||
<how-to-verify>
|
||||
1. Start dev servers: `bun run dev:server` and `bun run dev:client`
|
||||
2. Open http://localhost:5173 and navigate to a setup with items (or create one and add items)
|
||||
3. **Classification badges**: Verify each item card shows a gray pill badge. Click it and confirm it cycles: "Base Weight" -> "Worn" -> "Consumable" -> "Base Weight". Confirm clicking the badge does NOT open the item edit panel.
|
||||
4. **Classification persistence**: Refresh the page. Confirm classifications are preserved.
|
||||
5. **Weight subtotals**: With items classified differently, verify the summary card shows correct subtotals for Base, Worn, Consumable, and Total columns.
|
||||
6. **Donut chart (Category view)**: Verify the donut chart shows colored segments grouped by category. Hover segments to see tooltip with category name, weight, and percentage.
|
||||
7. **Donut chart (Classification view)**: Click the "Classification" pill toggle. Verify chart segments change to show base/worn/consumable breakdown with different colors. Hover to verify tooltips.
|
||||
8. **Donut center**: Confirm total weight is displayed in the center of the donut hole in the selected weight unit.
|
||||
9. **Weight unit**: Toggle the weight unit in the top bar (if available). Confirm all subtotals, chart center, and tooltips update to the new unit.
|
||||
10. **Add/remove items**: Add another item to the setup. Verify it appears with default "Base Weight" badge and the chart updates. Remove an item and verify classifications for remaining items are preserved.
|
||||
</how-to-verify>
|
||||
<verify>Visual verification by user following steps above</verify>
|
||||
<done>User confirms all classification badges, weight subtotals, donut chart, toggle, and tooltips work correctly</done>
|
||||
<resume-signal>Type "approved" to complete Phase 9, or describe any issues to address</resume-signal>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
```bash
|
||||
# Full test suite passes
|
||||
bun test
|
||||
|
||||
# Build succeeds
|
||||
bun run build
|
||||
|
||||
# Lint passes
|
||||
bun run lint
|
||||
```
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- WeightSummaryCard visible below sticky bar on setup detail page (only when items exist)
|
||||
- Four weight columns (Base, Worn, Consumable, Total) show correct values in selected unit
|
||||
- Donut chart renders with colored segments proportional to weight distribution
|
||||
- Pill toggle switches between category and classification chart views
|
||||
- Tooltip on hover shows segment name, formatted weight, and percentage
|
||||
- Total weight displayed in center of donut hole
|
||||
- Chart handles edge cases (no weight data, single category, etc.)
|
||||
- User confirms visual appearance matches expectations
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/09-weight-classification-and-visualization/09-02-SUMMARY.md`
|
||||
</output>
|
||||
Reference in New Issue
Block a user