How Plane Calculates Analytics and Burndown Charts: Server-Side Architecture Explained
Plane generates analytics and burndown charts using two core Python utilities in apps/api/plane/utils/analytics_plot.py that aggregate issue data by dimensions and calculate remaining work over time, returning dictionaries consumed by MobX stores in the React frontend.
The open-source project management platform Plane (makeplane/plane) handles its analytics visualizations through a centralized server-side calculation layer. These analytics and burndown chart calculations transform raw issue data into plot-ready structures using Django ORM aggregations, enabling both count-based and point-based progress tracking across Cycles and Modules.
Core Aggregation Utilities
Plane’s analytics engine relies on two primary helper functions that process QuerySets and return serialized data structures for chart rendering.
Dimension-Based Graphs via build_graph_plot
The build_graph_plot function generates bar charts and line graphs by grouping issues across configurable dimensions. It validates inputs against VALID_ANALYTICS_FIELDS for the x-axis (accepting values like state_id, labels__id, or created_at) and VALID_YAXIS for the y-axis (either issue_count or estimate).
The function normalizes date fields into string dimensions using annotate_with_monthly_dimension, creating "YYYY-MM" tags for temporal grouping. When a segment parameter is provided (such as priority), the data splits further into sub-categories. The aggregation layer then applies either Count("*") for issue tallies or Sum(Cast(...)) for estimate point totals, grouping results by dimension and applying priority-aware sorting. The output follows a nested dictionary structure: {dimension: [{segment?, count|estimate}, …]}.
from plane.utils.analytics_plot import build_graph_plot
distribution = build_graph_plot(
queryset=my_queryset,
x_axis="created_at", # month-grouped dimension
y_axis="issue_count",
segment="priority", # optional split
)
Burndown Calculations via burndown_plot
The burndown_plot function computes remaining work over the lifespan of a Cycle or Module. It first inspects Project.estimate.type to determine whether to use point-based or issue-count calculations, fetching total_estimate_points when "points" is configured; otherwise it defaults to total_issues.
The function constructs a date range from the entity’s start_date to end_date (or target_date for Modules). For each day, it queries completed items using TruncDate("completed_at") annotations—fetching point values when plot_type == "points" or counts otherwise. A running total tracks cumulative_pending_issues, subtracting daily completions from the initial total. Future dates beyond timezone.now() receive null values to terminate the chart at the current date. The result maps dates to remaining work: { "YYYY-MM-DD": remaining_work_or_None, … }, stored in the completion_chart field.
from plane.utils.analytics_plot import burndown_plot
chart = burndown_plot(
queryset=my_cycle, # Cycle or Module instance
slug="my-workspace",
project_id=42,
plot_type="points", # or "issues"
cycle_id="c123",
)
API Layer Integration Points
These utilities are invoked across multiple view modules to serve different analytics contexts:
- Module analytics: Called in
apps/api/plane/app/views/module/base.py(line 530) andarchive.py(line 423), returning bothdistributionfor bar charts andcompletion_chartfor burndown visualization. - Cycle analytics: Implemented in
apps/api/plane/app/views/cycle/base.py(line 932) andarchive.py(line 462), providing the same data structures scoped to Cycle boundaries. - Project-wide analytics: Utilized in
apps/api/plane/app/views/analytic/base.py(lines 68 and 214) for arbitrary dimension grouping across entire projects. - Export jobs: Background tasks in
apps/api/plane/bgtasks/analytic_plot_export.py(line 359) serialize these dictionaries for CSV/Excel generation.
Frontend Data Consumption
The React frontend consumes these pre-calculated structures through MobX state management. The stores apps/web/core/store/module.store.ts and cycle.store.ts expose a plotType property (defaulting to "burndown") and maintain chart data in the completion_chart observable.
UI components such as apps/web/core/components/modules/analytics-sidebar/issue-progress.tsx and the corresponding cycles component render the completion_chart dictionaries as line charts without additional client-side calculation.
// Rendering a burndown chart for a cycle in the UI
import { observer } from "mobx-react";
import { useStore } from "hooks/use-store";
const CycleBurndown = observer(({ cycleId }: { cycleId: string }) => {
const { cycleStore } = useStore();
const cycle = cycleStore.cycleMap[cycleId];
const chart = cycle?.estimate_distribution?.completion_chart ?? {};
return (
<LineChart
data={Object.entries(chart).map(([date, value]) => ({
date,
remaining: value,
}))}
/>
);
});
Practical Server-Side Example
To expose burndown data via a custom endpoint:
from plane.utils.analytics_plot import burndown_plot
from plane.db.models import Cycle
from django.http import JsonResponse
def get_cycle_burndown(request, cycle_id):
cycle = Cycle.objects.get(pk=cycle_id)
chart = burndown_plot(
queryset=cycle,
slug=cycle.project.workspace.slug,
project_id=cycle.project_id,
plot_type="issues",
cycle_id=cycle_id,
)
return JsonResponse(chart, safe=False)
Summary
- Plane calculates analytics server-side in
apps/api/plane/utils/analytics_plot.pyusingbuild_graph_plotfor dimension aggregations andburndown_plotfor time-series remaining work. - Validation constants
VALID_ANALYTICS_FIELDSandVALID_YAXISenforce supported grouping and metric types, whileannotate_with_monthly_dimensionhandles temporal normalization. - Burndown logic detects point-based estimation via
Project.estimate.type, iterates date ranges usingTruncDate, and stores results incompletion_chartfields. - API views in Module, Cycle, and Analytic base modules invoke these utilities at specific line numbers (530, 932, 68, 214) to populate responses.
- Frontend stores in
module.store.tsandcycle.store.tsconsume these dictionaries directly, rendering them inissue-progress.tsxcomponents without transformation.
Frequently Asked Questions
How does Plane handle date grouping in analytics charts?
Plane uses the annotate_with_monthly_dimension helper to transform datetime fields into "YYYY-MM" string tags when build_graph_plot receives date-related x-axis parameters. This enables monthly aggregation bars while preserving the ability to sort chronologically.
What is the difference between issue count and estimate points in burndown calculations?
When plot_type="points", the system checks Project.estimate.type and aggregates the sum of estimate values using Sum(Cast(...)). For plot_type="issues" (or when points are disabled), it uses Count("*") on the issue QuerySet. The burndown logic subtracts daily completed points or counts from the initial total_estimate_points or total_issues respectively.
Where does Plane store the calculated burndown chart data?
The burndown_plot function returns a dictionary mapping dates to remaining work values, which the API layer assigns to the completion_chart field on Cycle or Module model instances. This serialized structure persists in the database and transmits directly to the frontend MobX stores.
How does the frontend determine whether to display points or issues in the chart?
The MobX stores expose a plotType observable (defaulting to "burndown") that the UI components reference. When the user toggles between views, the store serves the appropriate pre-calculated data from completion_chart, ensuring the React components render the correct metric without recalculation.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →