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

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, while the frontend utilities in 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 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.


# 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

# 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.

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.

Snapshot vs. Live Data Sources

Plane prioritizes historical accuracy through the progress_snapshot JSON field defined in 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:

// 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 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%.

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:

// 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, which renders the visual chart. Components like CycleListItem utilize calculateCycleProgress directly for progress bar displays:

// 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, 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 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) 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →