# How Burndown Charts and Progress Metrics Are Calculated in Plane's Cycle Management

> Discover how Plane calculates burndown charts and progress metrics for effective cycle management. Understand your sprint progress with daily completions and ideal linear lines.

- Repository: [Plane/plane](https://github.com/makeplane/plane)
- Tags: internals
- Published: 2026-06-22

---

**Plane calculates burndown charts by aggregating completed issues or estimate points per day using `TruncDate` groupings, then subtracting cumulative completions from total scope to determine pending work, while progress metrics compute ideal linear completion lines and actual percentages using either live cycle data or stored progress snapshots.**

Plane's open-source project management platform (available at `makeplane/plane`) provides deterministic sprint analytics through a dual-layer calculation system. The backend generates raw burndown data in [`apps/api/plane/utils/analytics_plot.py`](https://github.com/makeplane/plane/blob/main/apps/api/plane/utils/analytics_plot.py), while the frontend utilities in [`packages/utils/src/cycle.ts`](https://github.com/makeplane/plane/blob/main/packages/utils/src/cycle.ts) format this data for visualization and compute completion percentages. Together, these components support both issue-based and point-based tracking with version-aware snapshot handling.

## Burndown Chart Calculation Logic

The burndown chart construction begins in the API layer when serializing cycle data. The `burndown_plot` function in [`apps/api/plane/utils/analytics_plot.py`](https://github.com/makeplane/plane/blob/main/apps/api/plane/utils/analytics_plot.py) generates a date-indexed mapping of remaining scope.

### Scope Identification and Date Range Generation

Plane first determines whether the chart tracks **issues** or **estimate points** via the `plot_type` parameter. The system queries the project's estimate configuration to verify if point-based tracking is enabled.

```python

# apps/api/plane/utils/analytics_plot.py

estimate_type = Project.objects.filter(
    workspace__slug=slug,
    pk=project_id,
    estimate__isnull=False,
    estimate__type="points",
).exists()

```

The function then generates a complete `date_range` spanning from the cycle's `start_date` to `end_date` (or `target_date` for modules). This creates the temporal axis for the chart.

### Completed Distribution Aggregation

For each day in the range, Plane aggregates completed work using Django's `TruncDate` to normalize timestamps to dates. The aggregation strategy differs by plot type:

- **Issues**: Counts completed issues per day
- **Points**: Sums `estimate_point__value` for completed issues per day

```python

# Aggregation truncated to date only

completed_distribution = Issue.issue_objects.filter(...).annotate(
    date=TruncDate("completed_at")
).values(...)

```

### Cumulative Pending Calculation

The core burndown logic walks the date range chronologically, maintaining a running total of completed work. For each date, it calculates pending scope by subtracting cumulative completions from the total scope.

```python
for date in date_range:
    total_completed = sum(
        float(item["estimate_point__value"])
        for item in completed_distribution
        if item["date"] and item["date"] <= date
    )
    pending = total_estimate_points - total_completed
    chart_data[str(date)] = None if date > timezone.now().date() else pending

```

Future dates receive `null` values, causing the UI to render a broken line that indicates projections versus actuals.

## Progress Metrics and Ideal Line Computation

While burndown charts show remaining work, progress metrics calculate completion percentages and ideal velocity lines using utilities in [`packages/utils/src/cycle.ts`](https://github.com/makeplane/plane/blob/main/packages/utils/src/cycle.ts).

### Snapshot vs. Live Data Sources

Plane prioritizes historical accuracy through the `progress_snapshot` JSON field defined in [`apps/api/plane/db/models/cycle.py`](https://github.com/makeplane/plane/blob/main/apps/api/plane/db/models/cycle.py). When issues transfer between cycles, Plane stores a snapshot of the distribution data to preserve historical metrics. The calculation logic checks for this snapshot first:

```typescript
// packages/utils/src/cycle.ts
const snapshot = cycle.progress_snapshot;
const details = snapshot && !isEmpty(snapshot) ? snapshot : cycle;

```

If no snapshot exists, the system falls back to live data from the cycle object.

### Ideal Line Formula

The ideal line represents linear progress from total scope to zero over the cycle duration. For each date `d`, Plane calculates:

```

ideal(d) = floor( (daysFromStart(d) / totalDays) * scope )

```

This yields the expected completion amount if work proceeds at a constant velocity. The `ideal` helper function in [`cycle.ts`](https://github.com/makeplane/plane/blob/main/cycle.ts) implements this formula, producing the straight-line expectation that appears in burndown visualizations.

### Actual Progress and Percentage Calculation

The `calculateCycleProgress` function computes completion percentages with optional inclusion of in-progress items. It guards against edge cases including zero totals, negative values, and percentages exceeding 100%.

```typescript
export const calculateCycleProgress = (
  cycle: ICycle | undefined,
  estimateType: "issues" | "points" = "issues",
  includeInProgress = false
): number => {
  if (!cycle) return 0;
  const snapshot = cycle.progress_snapshot;
  const details = snapshot && !isEmpty(snapshot) ? snapshot : cycle;

  const completed = estimateType === "points"
    ? (details.completed_estimate_points ?? 0) + 
      (includeInProgress ? details.started_estimate_points ?? 0 : 0)
    : (details.completed_issues ?? 0) + 
      (includeInProgress ? details.started_issues ?? 0 : 0);
  
  const cancelled = estimateType === "points"
    ? (details.cancelled_estimate_points ?? 0)
    : (details.cancelled_issues ?? 0);
    
  const total = estimateType === "points"
    ? (details.total_estimate_points ?? 0)
    : (details.total_issues ?? 0);

  const adjustedTotal = total - cancelled;
  if (adjustedTotal <= 0) return 0;
  const percentage = Math.round((completed / adjustedTotal) * 100);
  return Math.min(percentage, 100);
};

```

The calculation excludes cancelled items from the denominator and optionally adds started (in-progress) items to the completed numerator when `includeInProgress` is `true`.

## Frontend Rendering and Data Formatting

The React frontend consumes pre-computed chart data through the `formatActiveCycle` utility, which handles version compatibility between legacy v1 and current v2 cycle formats.

### formatActiveCycle Utility

This function determines whether the cycle uses the legacy single-distribution format (v1) or the newer per-day progress array (v2), then delegates to the appropriate formatter:

```typescript
// packages/utils/src/cycle.ts
export const formatActiveCycle = ({
  cycle,
  isBurnDown = false,
  isTypeIssue = true,
}: {
  cycle: ICycle;
  isBurnDown?: boolean;
  isTypeIssue?: boolean;
}) => {
  const endDate: Date | string = new Date(cycle.end_date!);
  return cycle.version === 1
    ? formatV1Data(isTypeIssue, cycle, isBurnDown, endDate)
    : formatV2Data(isTypeIssue, cycle, isBurnDown, endDate);
};

```

Both `formatV1Data` and `formatV2Data` invoke the `ideal` helper to generate the expected completion line, then assemble a uniform structure containing `date`, `scope`, `completed`, `pending`, `ideal`, and `actual` values for each day in the cycle.

### React Component Integration

The formatted data feeds into [`apps/web/core/components/cycles/analytics-sidebar/issue-progress.tsx`](https://github.com/makeplane/plane/blob/main/apps/web/core/components/cycles/analytics-sidebar/issue-progress.tsx), which renders the visual chart. Components like `CycleListItem` utilize `calculateCycleProgress` directly for progress bar displays:

```typescript
// apps/web/core/components/cycles/list/cycles-list-item.tsx
import { calculateCycleProgress } from "@plane/utils";

const CycleListItem = ({ cycle }) => {
  const progress = calculateCycleProgress(cycle);
  return <ProgressBar value={progress} />;
};

```

## Summary

- **Burndown charts** in Plane are generated by `burndown_plot` in [`apps/api/plane/utils/analytics_plot.py`](https://github.com/makeplane/plane/blob/main/apps/api/plane/utils/analytics_plot.py), which aggregates completed work by date using `TruncDate` and calculates remaining scope through cumulative subtraction.
- **Progress snapshots** stored in the `Cycle` model's `progress_snapshot` JSON field preserve historical metrics when issues move between sprints, ensuring accurate reporting even after data changes.
- **Ideal lines** calculate linear completion expectations using `floor( (daysFromStart / totalDays) * scope )`, providing the standard against which actual progress is measured.
- **Percentage calculations** exclude cancelled items from the total denominator and optionally include in-progress work, with strict guards against invalid values (negative, zero, or >100%).
- **Version-aware formatting** in [`packages/utils/src/cycle.ts`](https://github.com/makeplane/plane/blob/main/packages/utils/src/cycle.ts) ensures compatibility between legacy v1 and current v2 cycle data structures when preparing chart data for React components.

## Frequently Asked Questions

### How does Plane handle date ranges for cycles that haven't started yet?

Plane generates the complete date range from `start_date` to `end_date` regardless of current status, but sets chart values to `null` for any date after the current day. This allows the UI to render a broken line distinguishing historical actuals from future projections, preventing misleading trend lines for incomplete periods.

### What is the difference between burndown and burn-up calculations in Plane?

The calculation differs only in the **actual** value representation. For **burndown** charts, `actual` equals the `pending` (remaining) scope, decreasing toward zero. For **burn-up** charts, `actual` equals the `completed` amount, increasing toward the total scope. Both use the same `ideal` line calculation and underlying data aggregation.

### Can Plane calculate progress using story points instead of issue counts?

Yes. When `plot_type` is set to `"points"` and the project has an active point-based estimate system, Plane aggregates `estimate_point__value` from completed issues rather than counting issue cardinality. The `calculateCycleProgress` function accepts an `estimateType` parameter (`"issues"` or `"points"`) that switches between `total_issues`/`completed_issues` and `total_estimate_points`/`completed_estimate_points` fields.

### Where does Plane store historical cycle data when issues are transferred?

Plane stores a JSON `progress_snapshot` on the `Cycle` model ([`apps/api/plane/db/models/cycle.py`](https://github.com/makeplane/plane/blob/main/apps/api/plane/db/models/cycle.py)) when issues are transferred between cycles. This snapshot preserves the distribution data (completed counts, pending items, and date ranges) at the moment of transfer, allowing `calculateCycleProgress` to reference historical states rather than recalculating from current (modified) issue data.