Plane Cycle (Sprint) Architecture and Burndown Chart Calculation

Plane implements its Cycle feature through a three-layer architecture spanning React frontend components, MobX state management, and Django backend models, with burndown charts calculated by iterating daily through the sprint date range and subtracting cumulative completed issues or estimate points from the total scope.

Plane is an open-source project management platform that organizes work into Cycles—sprint-like containers for issues. Understanding how the Cycle feature is architected and how burndown analytics are computed helps developers customize the platform or integrate its API. This analysis examines the complete stack from the React UI components in apps/web/core/components/cycles to the Python analytics utilities in apps/api/plane/utils/analytics_plot.py that generate sprint progress visualizations.

Three-Layer Architecture Overview

Plane separates the Cycle feature into distinct frontend and backend layers. The frontend uses React with MobX for state management, while the backend relies on Django models and specialized analytics utilities to compute burndown data.

Frontend UI Components

The user interface lives in apps/web/core/components/cycles/ and renders cycle lists, detail pages, and analytics sidebars.

State Management and Services

The MobX store in apps/web/core/store/cycle.store.ts maintains UI state including the selected cycle, filters, and plot type preferences.

// apps/web/core/store/cycle.store.ts (excerpt)
@computed
getPlotTypeByCycleId = computedFn((cycleId: string) => this.plotType[cycleId] || "burndown");

The service layer wraps REST API calls. apps/web/core/services/cycle.service.ts provides methods to fetch cycle data:

// apps/web/core/services/cycle.service.ts (excerpt)
export class CycleService extends APIService {
  async list(slug: string, projectId: string) {
    return this.get(`/api/workspaces/${slug}/projects/${projectId}/cycles/`);
  }
}

Backend Models and ViewSets

The Django model in apps/api/plane/db/models/cycle.py defines the Cycle entity with standard fields (name, start_date, end_date) and a progress_snapshot JSON field that stores pre-computed analytics for fast retrieval.

The CycleViewSet in apps/api/plane/app/views/cycle/base.py handles CRUD operations and provides enriched querysets with annotated counts:


# apps/api/plane/app/views/cycle/base.py (excerpt)

.annotate(
    total_issues=Count(
        "issue_cycle__issue__id",
        distinct=True,
        filter=Q(
            issue_cycle__issue__archived_at__isnull=True,
            issue_cycle__issue__is_draft=False,
            issue_cycle__deleted_at__isnull=True,
            issue_cycle__issue__deleted_at__isnull=True,
        ),
    )
)

Burndown Chart Calculation Logic

The burndown chart computation occurs server-side and is delivered through the CycleAnalyticsEndpoint at GET /api/workspaces/:slug/projects/:projectId/cycles/:cycleId/analytics.

The Analytics Endpoint

This endpoint returns three data structures: assignee distribution, label distribution, and the completion chart (burndown). It invokes the burndown_plot function with a specified plot type:


# apps/api/plane/app/views/cycle/base.py (excerpt)

completion_chart = burndown_plot(
    queryset=cycle,
    slug=slug,
    project_id=project_id,
    plot_type="issues",   # or "points"

    cycle_id=cycle_id,
)

The Burndown Algorithm

The core logic resides in apps/api/plane/utils/analytics_plot.py. The function builds a daily timeline of remaining work by subtracting completed items from the total scope.

The calculation follows these steps:

  1. Determine the date range spanning from start_date to end_date.
  2. Collect completed work grouped by day—either issue counts or sum of estimate_point__value fields.
  3. Initialize the chart with the total scope (total_issues or total estimate points).
  4. Iterate chronologically through each date:
    • Calculate cumulative completed work up to that date.
    • Subtract from the total scope to get remaining work.
    • Store null for future dates (rendered as gaps in the UI).

# apps/api/plane/utils/analytics_plot.py (excerpt)

for date in date_range:
    cumulative_pending_issues = total_issues          # or total_estimate_points

    total_completed = sum(
        item["total_completed"] or float(item["estimate_point__value"])
        for item in completed_issues_distribution
        if item["date"] is not None and item["date"] <= date
    )
    cumulative_pending_issues -= total_completed
    chart_data[str(date)] = None if date > timezone.now().date() else cumulative_pending_issues

The resulting dictionary maps YYYY-MM-DD strings to remaining work values (or null), which the frontend renders as a line chart.

Frontend Integration and Data Flow

To retrieve burndown data in a client application, use the CycleAnalyticsService to call the analytics endpoint:

import { CycleAnalyticsService } from "@/services/cycle-analytics.service";

const analytics = new CycleAnalyticsService();
analytics.workspaceActiveCyclesAnalytics('my-workspace', 'project-123', 'cycle-456', 'issues')
  .then(data => {
    console.log('Assignees:', data.assignees);
    console.log('Labels:', data.labels);
    console.log('Burndown chart:', data.completion_chart);
  });

The completion_chart object can be fed directly to charting libraries. Here is an example using Recharts to render the sprint burndown:

import { LineChart, Line, XAxis, YAxis, Tooltip } from "recharts";

function Burndown({ data }: { data: Record<string, number | null> }) {
  const chartData = Object.entries(data).map(([date, value]) => ({
    date,
    remaining: value,
  }));

  return (
    <LineChart data={chartData} width={500} height={300}>
      <XAxis dataKey="date" />
      <YAxis />
      <Tooltip />
      <Line type="monotone" dataKey="remaining" stroke="#ff6600" />
    </LineChart>
  );
}

Summary

  • Plane's Cycle architecture consists of three layers: React UI components (apps/web/core/components/cycles), MobX stores and services (cycle.store.ts, cycle.service.ts), and Django backend models (cycle.py) with analytics utilities.
  • The burndown calculation happens in apps/api/plane/utils/analytics_plot.py via the burndown_plot function, which iterates through the sprint date range and subtracts cumulative completed work from the total scope.
  • The algorithm supports both issue count and estimate points as metrics, controlled by the plot_type parameter.
  • Future dates in the sprint return null values, creating natural gaps in the visualization until those dates occur.
  • Pre-computed analytics are stored in the progress_snapshot JSON field on the Cycle model for optimized read performance.

Frequently Asked Questions

How does Plane store pre-computed cycle analytics?

Plane stores pre-computed analytics in a progress_snapshot JSON field defined in apps/api/plane/db/models/cycle.py. This field caches calculated metrics to avoid expensive aggregations on every read, while the burndown_plot function in apps/api/plane/utils/analytics_plot.py generates time-series data dynamically based on current issue states.

What data sources feed the burndown chart calculation?

The burndown chart pulls from the issue_cycle relationships joined with the issue table. The calculation aggregates completed_at timestamps and optionally estimate_point__value fields from issues that are not archived, not drafts, and not soft-deleted, as filtered in the CycleViewSet annotations in apps/api/plane/app/views/cycle/base.py.

Can burndown charts track estimate points instead of issue counts?

Yes. The burndown_plot function accepts a plot_type parameter that accepts either "issues" or "points". When set to "points", the algorithm sums the estimate_point__value field for completed issues each day instead of counting issue records, allowing teams to track sprint progress by effort rather than item count.

How does the frontend handle future dates in burndown visualizations?

The backend returns null for any date beyond the current day (timezone.now().date()), and the frontend renders these as gaps in the line chart. This creates a clear visual distinction between historical actuals and projected future progress, preventing flat-line extensions into dates that have not yet occurred.

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 →