Skip to content

feat: enhance charts and modals with dynamic handling and user feedback#165

Merged
isotronic merged 10 commits into
masterfrom
fixes-updates
May 26, 2026
Merged

feat: enhance charts and modals with dynamic handling and user feedback#165
isotronic merged 10 commits into
masterfrom
fixes-updates

Conversation

@isotronic
Copy link
Copy Markdown
Owner

@isotronic isotronic commented May 26, 2026

Summary by Sourcery

Enhance chart interactions, app update handling, and workout history navigation for a smoother and more informative user experience.

New Features:

  • Add 1RM vs weight metric toggle and interactive tooltips to the exercise progression chart.
  • Add interactive tooltips and dynamic y‑axis scaling to body measurement charts.
  • Render an exercise progression chart within the exercise info history tab using historical non‑warmup sets.
  • Show per‑exercise global history for the current workout session based on the exercises in that session.
  • Allow the workout calendar modal to scroll back dynamically based on how far a user’s history goes.

Bug Fixes:

  • Ensure update errors distinguish between download and reload failures and present tailored user actions.
  • Prevent unnecessary update checks in development and add a retry-once strategy before surfacing errors.

Enhancements:

  • Normalize chart y‑axis ranges with padding for clearer visualization across metrics and measurements.
  • Standardize chart theming, pointer styling, and tooltip presentation across stats charts.
  • Refine workout stats date selection and calendar behavior using all completed workouts rather than a limited subset.

Summary by CodeRabbit

  • New Features

    • Exercise progression charts added to the History header with a time-range selector.
  • Improvements

    • Metric mode toggle (1RM vs raw weight) for weight charts.
    • Calendar now shows a broader set of workouts and supports deeper past scrolling.
    • Update modal fully localised with clearer error/retry actions.
    • Chart tooltips, pointer labels and spacing improved.
    • Performance optimisations on workout and history screens.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 26, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 01765ddd-2065-4475-8024-c993fd30fba9

📥 Commits

Reviewing files that changed from the base of the PR and between e8d0fe7 and 7324be2.

📒 Files selected for processing (1)
  • app/(app)/(tabs)/(stats)/history-details.tsx

📝 Walkthrough

Walkthrough

Adds chart-shaped exercise history and progression charts with metric toggles and pointer tooltips, refactors update-check errors to typed states with retry, changes global history queries to accept exerciseIds, memoises exercise IDs in workout views, and switches the Stats calendar to use allWorkouts with configurable past scroll range.

Changes

Update Error Handling Refactor

Layer / File(s) Summary
Update hook error type and retry logic
hooks/useAppUpdates.ts
useAppUpdates adds UpdateErrorType, replaces errorMessage with errorType, refactors check/fetch into an attempt(isRetry) helper with a one-time 5s retry on check failures, and exposes status, errorType, isUpdateReady, reloadApp, and dismissError.
Update modal UI and localisation
components/UpdateModal.tsx
UpdateModal reads errorType, builds localized content for downloading/ready/error states, branches on errorType in the error case to choose titles/messages/actions, and simplifies button wiring to content.onPress/content.buttonText.

Exercise Progression Chart with Metric Mode

Layer / File(s) Summary
Exercise history chart data interface
hooks/useExerciseHistoryQuery.ts
Adds ChartSet and extends ExerciseHistory with chartSets derived by filtering warmups/null metrics, defaulting nullable fields, and computing oneRepMax for weight-based tracking types.
Global exercise history query refactored for exercise IDs
hooks/useCompletedWorkoutsQuery.ts
fetchGlobalExerciseHistoryForSession now accepts exerciseIds, short-circuits on empty input, and SQL selects the latest completed workout per exercise using a correlated subquery; useGlobalExerciseHistoryForSessionQuery includes exerciseIds in its queryKey and enables only when non-empty.
Exercise detail tracking type handling
hooks/useExerciseDetailQuery.ts
Preserves nullable tracking_type by assigning trackingType directly instead of null-coalescing to an empty string.
Body measurement chart y-axis and pointer config
components/charts/BodyMeasurementLineChart.tsx
Computes yAxisOffset/yAxisMax with chart data, introduces POINTER_LABEL_WIDTH, rekeys LineChart on timeRange, and adds a pointerConfig with a pointer label component and tooltip styles.
Exercise progression chart with metric mode toggle
components/charts/ExerciseProgressionChart.tsx
Adds metricMode toggle for weight-type exercises, metric-aware baseline logic (1RM-only), empty-chart guard, recalculated y-axis scaling and datapoint styling, metric toggle UI, and pointer/tooltip rendering keyed by metricMode.
Exercise info screen chart header integration
app/(app)/exercise-info.tsx
Imports chart components and TrackedExerciseWithSets, adds timeRange state and distanceUnit, memoises chartExercise from historyData.chartSets, and conditionally renders a chart header (time-range selector + progression chart) in the History tab.

Workout Screen Optimisations and Calendar Refinements

Layer / File(s) Summary
Workout overview screen exercise IDs memoisation
app/(app)/(workout)/index.tsx
Memoises exerciseIds from workout?.exercises and passes it to useGlobalExerciseHistoryForSessionQuery to avoid unnecessary recomputation.
Stats screen calendar data source and range computation
app/(app)/(tabs)/(stats)/index.tsx
Switches the calendar data source from time-range-scoped completedWorkouts to allWorkouts, groups allWorkoutsByDate, derives markedDates from that grouping, computes calendarPastScrollRange via differenceInCalendarMonths from the oldest workout to now, and updates calendar open logic to use allWorkoutsByDate.
Workout calendar modal scroll range support
components/stats/WorkoutCalendarModal.tsx
Adds optional pastScrollRange?: number (default 24), memoises handleDayPress to forward DateData.dateString to onDayPress, and configures CalendarList with pastScrollRange and futureScrollRange={1}.

Sequence Diagram(s)

sequenceDiagram
  participant Screen as ExerciseInfoScreen
  participant History as useExerciseHistoryQuery
  participant ProgressChart as ExerciseProgressionChart
  participant BodyChart as BodyMeasurementLineChart

  Screen->>History: fetch history (sections + chartSets)
  History-->>Screen: return sections + chartSets
  Screen->>Screen: build chartExercise from chartSets
  Screen->>ProgressChart: pass chartExercise + timeRange + units
  ProgressChart->>ProgressChart: toggle metricMode (weight ↔ 1RM)
  ProgressChart->>ProgressChart: compute chart points, y-axis, pointers
  ProgressChart-->>Screen: render with metric toggle and tooltips
  Screen->>BodyChart: pass body measurement data + timeRange
  BodyChart->>BodyChart: compute yAxisOffset/yAxisMax and pointer labels
  BodyChart-->>Screen: render with pointer tooltips
Loading
sequenceDiagram
  participant Modal as UpdateModal
  participant Hook as useAppUpdates
  participant Expo as Updates

  Hook->>Expo: checkForUpdateAsync()
  alt update available
    Expo-->>Hook: update available
    Hook->>Expo: fetchUpdateAsync()
    alt download succeeds
      Expo-->>Hook: set isUpdateReady
    else download fails
      Hook->>Hook: set errorType = "download-failed"
      Hook->>Modal: expose status + errorType
    end
  else check fails (first)
    Hook->>Hook: retry after 5s (attempt(isRetry=true))
  else check fails (second)
    Hook->>Hook: set status = "no-update"
  end

  Hook-->>Modal: expose status + errorType
  Modal->>Modal: switch on errorType to select UI and button action
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • isotronic/MuscleQuest#158: Calendar modal and Stats screen calendar wiring that this PR extends by switching StatsScreen to allWorkouts and adding pastScrollRange.
  • isotronic/MuscleQuest#147: Prior exercise history work that this PR extends by adding chartSets and integrating the progression chart into exercise-info.
  • isotronic/MuscleQuest#163: Earlier global exercise history plumbing related to useGlobalExerciseHistoryForSessionQuery that this PR refactors to accept exerciseIds.

"I hopped through sets and plotted skies,
toggled metrics with bright little eyes,
typed errors now named with care,
calendars scroll through workouts there,
a rabbit cheers — your charts arise!"

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title describes a general enhancement to charts and modals, but it oversimplifies the scope—the changeset includes specific features like metric mode toggle, error type distinction, calendar scroll ranges, and exercise history display.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 4 issues, and left some high level feedback:

  • Both ExerciseProgressionChart and BodyMeasurementLineChart use n inside pointerLabelComponent (idx === n - 1) but n is not defined in scope, which will throw at runtime; you likely want const n = chartData.length or equivalent in the closure.
  • In UpdateModal, content.onPress is used unconditionally when rendering the button, but content cases without showButton don’t set onPress; tightening the content type (e.g. a discriminated union keyed on showButton) will prevent accidental missing handlers.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Both `ExerciseProgressionChart` and `BodyMeasurementLineChart` use `n` inside `pointerLabelComponent` (`idx === n - 1`) but `n` is not defined in scope, which will throw at runtime; you likely want `const n = chartData.length` or equivalent in the closure.
- In `UpdateModal`, `content.onPress` is used unconditionally when rendering the button, but `content` cases without `showButton` don’t set `onPress`; tightening the `content` type (e.g. a discriminated union keyed on `showButton`) will prevent accidental missing handlers.

## Individual Comments

### Comment 1
<location path="components/charts/ExerciseProgressionChart.tsx" line_range="505" />
<code_context>
+            const val = items[0]?.value;
+            if (val == null) return null;
+            const display = Number.isInteger(val) ? `${val}` : val.toFixed(1);
+            const isLast = idx === n - 1 && n > 1;
+            return (
+              <View style={[styles.tooltip, isLast && styles.tooltipLast]}>
</code_context>
<issue_to_address>
**issue (bug_risk):** The `n` variable used in `pointerLabelComponent` is undefined, which will throw at runtime.

In `pointerLabelComponent`, `const isLast = idx === n - 1 && n > 1;` uses `n`, which isn’t defined in this scope, so the tooltip will throw a ReferenceError on first render. If this is meant to be the total number of points, derive it once (e.g. `const n = chartData.length;` in the outer scope or inside the `useMemo` that builds `chartData`) and close over it here.
</issue_to_address>

### Comment 2
<location path="components/charts/BodyMeasurementLineChart.tsx" line_range="317" />
<code_context>
+            const val = items[0]?.value;
+            if (val == null) return null;
+            const display = Number.isInteger(val) ? `${val}` : val.toFixed(1);
+            const isLast = idx === n - 1 && n > 1;
+            return (
+              <View style={[styles.tooltip, isLast && styles.tooltipLast]}>
</code_context>
<issue_to_address>
**issue (bug_risk):** The `n` variable in the body measurement chart tooltip is also undefined and will crash when the tooltip renders.

Here `pointerLabelComponent` uses `n` without defining it, which will throw a ReferenceError when the tooltip renders. You likely want to compare `idx` with the last index of `chartData`/`dataPoints`, e.g. compute `const n = chartData.length;` in the component body and close over it in the callback.
</issue_to_address>

### Comment 3
<location path="components/charts/ExerciseProgressionChart.tsx" line_range="246" />
<code_context>
   const { width: screenWidth } = useWindowDimensions();

-  const chartData = useMemo(() => {
+  const { chartData, yAxisOffset, yAxisMax } = useMemo(() => {
     const buckets = groupMeasurementsByTime(data, timeRange);
     // Only include buckets with actual data — measurements are point-in-time
</code_context>
<issue_to_address>
**issue (complexity):** Consider extracting the chart calculation, pointer configuration, and metric toggle logic into small helper functions/components so `ExerciseProgressionChart` remains a thin orchestrator instead of holding all cross-cutting behavior inline.

The new features are good, but they’ve made `ExerciseProgressionChart` carry a lot of cross‑cutting logic. You can keep all behavior and reduce complexity by extracting a few focused helpers.

### 1. Break up the `useMemo` closure

The current `useMemo` handles:
- metric mode → sets remapping
- bucketing + forward fill
- baseline application
- y‑axis range calculation
- dataPoint shaping + tooltip support (`hasData`)

You can preserve behavior by pulling this into small pure helpers and keeping `useMemo` as orchestration. For example:

```ts
type MetricMode = "1rm" | "weight";

type ChartPoint = {
  value: number;
  label: string | undefined;
  labelComponent: (() => React.JSX.Element) | undefined;
  dataPointColor: string;
  dataPointRadius: number;
  hasData: boolean;
};

type ChartCalcResult = {
  chartData: ChartPoint[];
  yAxisOffset: number;
  yAxisMax: number | undefined;
};

const EMPTY_CHART_RESULT: ChartCalcResult = {
  chartData: [],
  yAxisOffset: 0,
  yAxisMax: undefined,
};

function buildSetsForChart(
  sets: CompletedSet[],
  metricMode: MetricMode,
  showMetricToggle: boolean,
): CompletedSet[] {
  if (metricMode !== "weight" || !showMetricToggle) return sets;
  return sets.map((s) => ({
    ...s,
    progressionMetric: s.weight,
  }));
}

function applyForwardFill(buckets: ReturnType<typeof groupSetsByTime>) {
  let lastValue: number | null = null;
  for (let i = 0; i < buckets.length; i++) {
    if (buckets[i].hasData) {
      lastValue = buckets[i].value;
    } else if (lastValue !== null) {
      buckets[i] = { ...buckets[i], value: lastValue };
    }
  }
}

function applyBaseline({
  buckets,
  metricMode,
  preRangeBaseline,
  isWeightTypePre,
  conversionFactor,
}: {
  buckets: ReturnType<typeof groupSetsByTime>;
  metricMode: MetricMode;
  preRangeBaseline: number | null | undefined;
  isWeightTypePre: boolean;
  conversionFactor: number;
}) {
  const rawBaseline =
    metricMode === "1rm" && preRangeBaseline != null
      ? isWeightTypePre
        ? preRangeBaseline * conversionFactor
        : preRangeBaseline
      : undefined;

  const baselineValue =
    rawBaseline != null && rawBaseline > 0 ? rawBaseline : undefined;

  if (baselineValue == null) return;

  for (let i = 0; i < buckets.length; i++) {
    if (buckets[i].value === null) {
      buckets[i] = { ...buckets[i], value: baselineValue };
    }
  }
}

function bucketsToChartData(
  buckets: ReturnType<typeof groupSetsByTime>,
): ChartPoint[] {
  return buckets.map((bucket) => {
    const metric = bucket.value ?? 0;
    const labelComponent = bucket.labelLine2
      ? () => (
          <View style={styles.twoLineLabel}>
            <Text style={styles.twoLineLabelText}>{bucket.label}</Text>
            <Text style={styles.twoLineLabelText}>{bucket.labelLine2}</Text>
          </View>
        )
      : undefined;

    return {
      value: metric,
      label: labelComponent ? undefined : bucket.label,
      labelComponent,
      dataPointColor: bucket.hasData ? Colors.dark.tint : "transparent",
      dataPointRadius: 4,
      hasData: bucket.hasData,
    };
  });
}

function computeYAxisRange(values: number[]): {
  offset: number;
  max: number;
} {
  const minVal = Math.min(...values);
  const maxVal = Math.max(...values);

  if (minVal > 0) {
    const range = maxVal - minVal;
    const padding = Math.max(range * 0.15, 0.5);
    const offset = minVal - padding;
    const max = maxVal + padding - offset;
    return { offset, max };
  }

  const padding = Math.max(maxVal * 0.15, 0.5);
  return { offset: 0, max: maxVal + padding };
}
```

Then `useMemo` becomes mostly wiring, which is much easier to mentally follow and unit‑test:

```ts
const { chartData, yAxisOffset, yAxisMax } = useMemo<ChartCalcResult>(() => {
  const setsForChart = buildSetsForChart(
    exercise.completed_sets,
    metricMode,
    showMetricToggle,
  );

  const buckets = groupSetsByTime(
    setsForChart,
    timeRange,
    exercise.tracking_type,
    conversionFactor,
  );

  if (buckets.length === 0) return EMPTY_CHART_RESULT;

  applyForwardFill(buckets);

  const isWeightTypePre =
    exercise.tracking_type === null ||
    exercise.tracking_type === "weight" ||
    exercise.tracking_type === "assisted";

  applyBaseline({
    buckets,
    metricMode,
    preRangeBaseline,
    isWeightTypePre,
    conversionFactor,
  });

  if (!buckets.some((b) => b.value !== null)) return EMPTY_CHART_RESULT;

  const chartData = bucketsToChartData(buckets);
  const vals = chartData.map((p) => p.value);
  const { offset, max } = computeYAxisRange(vals);

  return { chartData, yAxisOffset: offset, yAxisMax: max };
}, [
  exercise.completed_sets,
  exercise.tracking_type,
  timeRange,
  conversionFactor,
  preRangeBaseline,
  metricMode,
  showMetricToggle,
]);
```

This keeps behavior identical but isolates baseline/forward‑fill/range logic into small helpers and makes the `EMPTY_CHART_RESULT` type explicit.

### 2. Extract pointer / tooltip configuration

The inline `pointerConfig` closure is quite dense and closes over `chartData`, `n`, and `tooltipUnit`. Extracting it into a helper keeps the JSX readable:

```ts
type PointerConfigProps = {
  chartData: ChartPoint[];
  pointCount: number;
  tooltipUnit: string;
};

function buildExercisePointerConfig({
  chartData,
  pointCount,
  tooltipUnit,
}: PointerConfigProps) {
  return {
    activatePointersInstantlyOnTouch: true,
    persistPointer: true,
    showPointerStrip: true,
    pointerStripColor: "rgba(255,255,255,0.15)",
    pointerStripWidth: 1,
    pointerColor: Colors.dark.highlight,
    radius: 5,
    pointerLabelWidth: POINTER_LABEL_WIDTH,
    pointerLabelHeight: 34,
    autoAdjustPointerLabelPosition: true,
    shiftPointerLabelY: -44,
    pointerLabelComponent: (
      items: { value: number }[],
      _secondary: unknown,
      idx: number,
    ) => {
      if (!chartData[idx]?.hasData) return null;

      const val = items[0]?.value;
      if (val == null) return null;

      const display = Number.isInteger(val) ? `${val}` : val.toFixed(1);
      const isLast = idx === pointCount - 1 && pointCount > 1;

      return (
        <View style={[styles.tooltip, isLast && styles.tooltipLast]}>
          <Text style={styles.tooltipText}>
            {tooltipUnit ? `${display} ${tooltipUnit}` : display}
          </Text>
        </View>
      );
    },
  } as const;
}
```

Usage becomes much cleaner:

```tsx
const n = chartData.length;

<LineChart
  // ...
  yAxisOffset={yAxisOffset}
  maxValue={yAxisMax}
  pointerConfig={buildExercisePointerConfig({
    chartData,
    pointCount: n,
    tooltipUnit,
  })}
/>
```

You keep all tooltip behavior but remove a large nested object from the JSX.

### 3. Move the metric toggle into a tiny component

The toggle’s layout/styling is mixed into the main component. Extracting it into a dumb presentational component leaves `ExerciseProgressionChart` concerned only with the selected mode:

```ts
type MetricModeToggleProps = {
  mode: MetricMode;
  onChange: (mode: MetricMode) => void;
};

const MetricModeToggle: React.FC<MetricModeToggleProps> = ({ mode, onChange }) => (
  <View style={styles.metricToggleRow}>
    {(["1rm", "weight"] as const).map((m) => {
      const active = mode === m;
      const label = m === "1rm" ? "1RM" : t`Weight`;

      return (
        <TouchableOpacity
          key={m}
          onPress={() => onChange(m)}
          style={[styles.metricPill, active && styles.metricPillActive]}
          activeOpacity={0.7}
        >
          <Text
            style={[
              styles.metricPillLabel,
              active && styles.metricPillLabelActive,
            ]}
          >
            {label}
          </Text>
        </TouchableOpacity>
      );
    })}
  </View>
);
```

Then in the main component:

```tsx
{showMetricToggle && (
  <MetricModeToggle mode={metricMode} onChange={setMetricMode} />
)}
```

This keeps all current functionality but prevents `ExerciseProgressionChart` from becoming agodcomponent and makes each concern (data shaping, yaxis range, tooltip behavior, UI toggle) testable and easier to reason about.
</issue_to_address>

### Comment 4
<location path="components/charts/BodyMeasurementLineChart.tsx" line_range="214" />
<code_context>
   const { width: screenWidth } = useWindowDimensions();

-  const chartData = useMemo(() => {
+  const { chartData, yAxisOffset, yAxisMax } = useMemo(() => {
     const buckets = groupMeasurementsByTime(data, timeRange);
     // Only include buckets with actual data — measurements are point-in-time
</code_context>
<issue_to_address>
**issue (complexity):** Consider extracting the Y-axis range math and tooltip renderer into small helpers and passing `n` explicitly to simplify the chart component and make the logic reusable.

You can reduce the added complexity without changing behavior by extracting two small helpers and making the `n` dependency explicit.

### 1. Extract Yaxis range computation

Right now the main `useMemo` computes both the data points and the yaxis range. You can keep functionality identical while separating concerns slightly:

```ts
const computeYAxisRange = (values: number[]) => {
  const minVal = Math.min(...values);
  const maxVal = Math.max(...values);
  const range = maxVal - minVal;
  const padding = Math.max(range * 0.15, 0.5);
  const yMin = minVal - padding;
  const yMax = maxVal + padding;

  return { yAxisOffset: yMin, yAxisMax: yMax - yMin };
};
```

Then reuse it inside the memo:

```ts
const { chartData, yAxisOffset, yAxisMax } = useMemo(() => {
  const buckets = groupMeasurementsByTime(data, timeRange);
  const dataPoints = buckets.filter((b) => b.hasData);
  if (dataPoints.length === 0) {
    return { chartData: [], yAxisOffset: 0, yAxisMax: 100 };
  }

  const values = dataPoints.map((b) => b.value ?? 0);
  const { yAxisOffset, yAxisMax } = computeYAxisRange(values);

  const chartData = dataPoints.map((bucket) => {
    const labelComponent = bucket.labelLine2
      ? () => (
          <View style={styles.twoLineLabel}>
            <Text style={styles.twoLineLabelText}>{bucket.label}</Text>
            <Text style={styles.twoLineLabelText}>{bucket.labelLine2}</Text>
          </View>
        )
      : undefined;

    return {
      value: bucket.value ?? 0,
      label: labelComponent ? undefined : bucket.label,
      labelComponent,
      dataPointColor: Colors.dark.tint,
      dataPointRadius: 4,
    };
  });

  return { chartData, yAxisOffset, yAxisMax };
}, [data, timeRange]);
```

This keeps the logic the same but makes the Yaxis math easier to reuse (e.g., in `ExerciseProgressionChart`).

### 2. Extract tooltip renderer and make `n` explicit

The tooltip logic is long and closes over `n`. You can both shorten the JSX and make the dependency explicit by extracting a small renderer:

```ts
const renderMeasurementTooltip = (
  unit: string,
  n: number,
): ((items: { value: number }[], _secondary: unknown, idx: number) => JSX.Element | null) =>
  (items, _secondary, idx) => {
    const val = items[0]?.value;
    if (val == null) return null;

    const display = Number.isInteger(val) ? `${val}` : val.toFixed(1);
    const isLast = idx === n - 1 && n > 1;

    return (
      <View style={[styles.tooltip, isLast && styles.tooltipLast]}>
        <Text style={styles.tooltipText}>
          {display} {unit}
        </Text>
      </View>
    );
  };
```

Then use it in the chart, passing `n` explicitly:

```tsx
const n = chartData.length;

<LineChart
  key={timeRange}
  data={chartData}
  width={chartWidth}
  spacing={spacing}
  initialSpacing={initialSpacing}
  endSpacing={initialSpacing}
  // ...
  yAxisOffset={yAxisOffset}
  maxValue={yAxisMax}
  pointerConfig={{
    activatePointersInstantlyOnTouch: true,
    persistPointer: true,
    showPointerStrip: true,
    pointerStripColor: "rgba(255,255,255,0.15)",
    pointerStripWidth: 1,
    pointerColor: Colors.dark.highlight,
    radius: 5,
    pointerLabelWidth: POINTER_LABEL_WIDTH,
    pointerLabelHeight: 34,
    autoAdjustPointerLabelPosition: true,
    shiftPointerLabelY: -44,
    pointerLabelComponent: renderMeasurementTooltip(unit, n),
  }}
/>
```

If `ExerciseProgressionChart` uses the same pointer behavior, you can move `renderMeasurementTooltip` (and/or a `buildPointerConfig`) into a shared module and parametrize `unit` / formatting, which will reduce duplication across both charts while keeping all behavior unchanged.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread components/charts/ExerciseProgressionChart.tsx
Comment thread components/charts/BodyMeasurementLineChart.tsx
Comment thread components/charts/ExerciseProgressionChart.tsx
Comment thread components/charts/BodyMeasurementLineChart.tsx
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@hooks/useExerciseDetailQuery.ts`:
- Line 156: The code is incorrectly forcing a nullable trackingType to string
(tracking_type: trackingType as string) which contradicts the downstream
handling and the actual nullable semantics; update useExerciseDetailQuery (and
the historyData.trackingType usage in app/(app)/exercise-info.tsx) to preserve
the nullable type (pass tracking_type: trackingType or explicitly typed as
string | null) and update the shared type TrackedExerciseWithSets or its
consumers so tracking_type is consistently string | null across the codebase,
ensuring all callers (e.g., hooks/useTrackedExercisesQuery.ts and
components/charts/ExerciseProgressionChart.tsx) accept the nullable value rather
than casting away nullability.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 81b911cb-33ee-4143-8992-bdb56e351a0d

📥 Commits

Reviewing files that changed from the base of the PR and between eb88296 and 55a9825.

📒 Files selected for processing (11)
  • app/(app)/(tabs)/(stats)/index.tsx
  • app/(app)/(workout)/index.tsx
  • app/(app)/exercise-info.tsx
  • components/UpdateModal.tsx
  • components/charts/BodyMeasurementLineChart.tsx
  • components/charts/ExerciseProgressionChart.tsx
  • components/stats/WorkoutCalendarModal.tsx
  • hooks/useAppUpdates.ts
  • hooks/useCompletedWorkoutsQuery.ts
  • hooks/useExerciseDetailQuery.ts
  • hooks/useExerciseHistoryQuery.ts

Comment thread hooks/useExerciseDetailQuery.ts Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/(app)/(tabs)/(stats)/history-details.tsx (1)

288-292: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Inconsistent null handling for set.reps.

On line 278, set.reps ?? 0 guards against null values when displaying reps for the "reps" tracking type. However, lines 290 and 298 display {set.reps} directly without null coalescing for "weight" and "assisted" tracking types. If set.reps is null (as confirmed by the upstream contract in index.tsx), these lines would render "null Reps" or display unexpectedly. Apply consistent null handling across all tracking types.

🛡️ Proposed fix for consistent null handling
                     <ThemedText style={styles.setText}>
                       <Trans>
-                          {set.weight} {settings?.weightUnit} | {set.reps} Reps
+                          {set.weight} {settings?.weightUnit} | {set.reps ?? 0} Reps
                       </Trans>
                     </ThemedText>
                     <ThemedText style={styles.setText}>
                       <Trans>
                         Assist {set.weight} {settings?.weightUnit} | Resist{" "}
                         {bodyWeight - (set.weight || 0)}{" "}
-                          {settings?.weightUnit} | {set.reps} Reps
+                          {settings?.weightUnit} | {set.reps ?? 0} Reps
                       </Trans>
                     </ThemedText>

Also applies to: 294-300

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/`(app)/(tabs)/(stats)/history-details.tsx around lines 288 - 292, Several
JSX renderings in history-details.tsx use {set.reps} directly for the "weight"
and "assisted" tracking types (inside ThemedText elements that also show
settings?.weightUnit), which can render "null Reps"; update those occurrences to
use null-coalescing (set.reps ?? 0) like the "reps" case. Locate the ThemedText
nodes that show "{set.weight} {settings?.weightUnit} | {set.reps} Reps" and the
similar assisted/tracking-type blocks and replace {set.reps} with {set.reps ??
0}; also scan the component for any other direct uses of set.reps and apply the
same fix.
🧹 Nitpick comments (1)
app/(app)/(tabs)/(stats)/history-details.tsx (1)

277-281: 💤 Low value

Consider alternative display for null reps.

Whilst set.reps ?? 0 correctly ensures the <Plural> component receives a numeric value, displaying "0 Reps" when reps is null may not convey the intended meaning. Consider whether rendering "—" or conditionally omitting the display when set.reps is null would provide clearer feedback to users.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/`(app)/(tabs)/(stats)/history-details.tsx around lines 277 - 281, The
current render uses <Plural value={set.reps ?? 0} ... /> which forces a "0 Reps"
display when set.reps is null; change the rendering logic in history-details.tsx
to treat null/undefined differently by conditionally rendering the <Plural>
component only when set.reps is a number (e.g., check Number.isFinite(set.reps)
or set.reps != null) and otherwise render a placeholder like "—" or omit the
element entirely; update the JSX around the <Plural> usage so the UI shows the
placeholder/hidden state for null reps instead of "0 Reps".
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@app/`(app)/(tabs)/(stats)/history-details.tsx:
- Around line 288-292: Several JSX renderings in history-details.tsx use
{set.reps} directly for the "weight" and "assisted" tracking types (inside
ThemedText elements that also show settings?.weightUnit), which can render "null
Reps"; update those occurrences to use null-coalescing (set.reps ?? 0) like the
"reps" case. Locate the ThemedText nodes that show "{set.weight}
{settings?.weightUnit} | {set.reps} Reps" and the similar assisted/tracking-type
blocks and replace {set.reps} with {set.reps ?? 0}; also scan the component for
any other direct uses of set.reps and apply the same fix.

---

Nitpick comments:
In `@app/`(app)/(tabs)/(stats)/history-details.tsx:
- Around line 277-281: The current render uses <Plural value={set.reps ?? 0} ...
/> which forces a "0 Reps" display when set.reps is null; change the rendering
logic in history-details.tsx to treat null/undefined differently by
conditionally rendering the <Plural> component only when set.reps is a number
(e.g., check Number.isFinite(set.reps) or set.reps != null) and otherwise render
a placeholder like "—" or omit the element entirely; update the JSX around the
<Plural> usage so the UI shows the placeholder/hidden state for null reps
instead of "0 Reps".

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 5c1c948f-5614-418a-a827-ffa09430e9a0

📥 Commits

Reviewing files that changed from the base of the PR and between 55a9825 and e8d0fe7.

📒 Files selected for processing (4)
  • app/(app)/(tabs)/(stats)/history-details.tsx
  • app/(app)/exercise-info.tsx
  • hooks/useExerciseDetailQuery.ts
  • hooks/useTrackedExercisesQuery.ts
✅ Files skipped from review due to trivial changes (1)
  • hooks/useTrackedExercisesQuery.ts

@isotronic isotronic merged commit b843291 into master May 26, 2026
3 checks passed
@isotronic isotronic deleted the fixes-updates branch May 26, 2026 09:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant