Skip to main content

Status (2026-06-12): Tier 1 + Tier 2 done — all hand-rolled charts live in @enode/ui/charts/ (bar-fill, technique-line-chart, technique-trajectory-chart, performance-chart, metric-bar). Storybook + TSDoc added; npm run docs:check green. The Phase 5 cleanup (deleting the dead Chart.js iframe pair) is intentionally left undone per request.

Promote the hand-rolled charts to @enode/ui

The tracking app renders its data visualizations with a mix of techniques. The hand-rolled charts (no third-party library) are good shared-library candidates because their data layer already lives in @enode/core and their only dependencies are @enode/core + @enode/ui. This plan promotes them so a future portal session-review / athlete-performance view can reuse them.

Out of scope: the Chart.js iframe charts (technique-chart.tsx, technique-bar-path.tsx) — they are dead code (zero importers), superseded by the native-SVG technique charts. They should be deleted, not promoted (see Cleanup).

Why it's feasible

  1. The data layer is already shared. set-chart-service.ts and rep-chart-service.ts live in packages/core/src/training/. Only the rendering moves.
  2. Every dependency is already shareable. The charts import only @enode/core (types, useUnits, useTextContent, useMetricCategory, metric category colors, display/metrics stores) and @enode/ui (LottieCircle). @enode/ui already depends on @enode/core.
  3. No i18n, no app glue. None carry t() (entity names come from useTextContent; axis labels are raw unit symbols). Cross-chart crosshair sync is prop-driven lifted state, not infrastructure coupling.

Inventory & tiers

Tier 1 — pure SVG, zero coupling (lift-and-shift)

ComponentFileLinesDepends only on
repBarFillStyletoday/rep-bar-fill.ts33RepPhase type
TechniqueLineCharttoday/technique-line-chart.tsx854ChartPoint/PhaseSegment types + phaseLabel
TechniqueTrajectoryCharttoday/technique-trajectory-chart.tsx528TrajectoryPoint type

Both SVG charts receive their data and the shared crosshairTime + onCrosshairChange via props; the parent (technique-tab.tsx) owns the crosshair state. So they move with no app-local import fixes — only the importer repoints.

Tier 2 — core-hook-coupled render components (shareable, more care)

ComponentFileCoupling
MetricBartoday/focus-metric.tsxNone — already pure (bars/marker/max props)
FocusMetricHeadertoday/focus-metric.tsxuseUnits/useTextContent/useMetricCategory + LottieCircle
PerformanceCharttoday/performance-chart.tsxsame core hooks + colors + LottieCircle
PerformanceTabtoday/performance-tab.tsxuseFocusMetrics (reads userAppSettings)

The only app-flavored piece is useFocusMetrics — the selection of which metrics are "focus" (reads userAppSettings). That is a data/config concern, not rendering.

Design principle: promote rendering, parameterize selection

Mirrors the settings-drawer promotion. The render components move to @enode/ui; the container keeps choosing the session/metrics and feeds the already-computed service output (MetricPerformanceData[], focus IDs, LineSeriesSpec[]) in. PerformanceTab's useFocusMetrics selection stays app-side (or becomes an opt-in hook).

Target layout

packages/ui/src/charts/
bar-fill.ts # repBarFillStyle (from rep-bar-fill.ts)
technique-line-chart.tsx
technique-trajectory-chart.tsx
performance-chart.tsx # + PerformanceMode (Tier 2)
metric-bar.tsx # MetricBar + FocusMetricHeader, render only (Tier 2)
*.stories.tsx

Wildcard exports (./*./src/*) mean no package.json change.

Phases

  1. Tier 1 (foundation): bar-fill.ts + the two SVG technique charts → @enode/ui/charts/; repoint technique-tab.tsx and the rep-bar-fill importers. Lowest risk, highest LOC.
  2. Tier 2a — PerformanceChart@enode/ui/charts/; PerformanceTab keeps the focus selection and renders the shared chart.
  3. Tier 2b — MetricBar + FocusMetricHeader: split the render components out of focus-metric.tsx (7 importers — same re-export care as the earlier FOCUS_CATEGORY_KEY extraction), leaving useFocusMetrics/useFocusMetric in tracking.
  4. Storybook + TSDoc: charts are ideal Storybook subjects (prop-driven, mockable). Build fixture data (no live APIs) for each; TSDoc the exported props. Required by the docs rules.
  5. Cleanup (separate): delete the dead Chart.js iframe pair — technique-chart.tsx, technique-bar-path.tsx, and public/technique-*.html (~3,500 lines).

Risks

  • focus-metric.tsx is a 7-importer multi-export file — the MetricBar / FocusMetricHeader split needs careful re-exports (as done for FOCUS_CATEGORY_KEY).
  • Storybook fixtures are the bulk of Phase 4 — the chart data types need hand-built mock data (straightforward since the services are pure).
  • No portal consumer yet: this is promotion for reuse-readiness, not an immediate feature. The shared charts must stay prop-driven so the portal can feed its own session data.