---
title: "India GST Collection Analysis"
subtitle: "Interactive Dashboard — Financial Years 2017–18 to 2024–25"
author: "GST Analytics"
date: "2026-04-13"
date-modified: last-modified
format:
html:
theme:
- cosmo
- custom.scss # optional — drop if not using Sass overrides
toc: true
toc-depth: 3
toc-title: "Contents"
toc-location: left
number-sections: false
smooth-scroll: true
code-fold: true
code-tools: true
code-summary: "Show code"
fig-width: 11
fig-height: 6
fig-align: center
embed-resources: true # single self-contained HTML file
page-layout: full
df-print: paged
execute:
echo: true
warning: false
message: false
cache: false
knitr:
opts_chunk:
comment: "#>"
---
```{r setup}
#| label: setup
#| include: false
## ── Libraries ────────────────────────────────────────────────
library(data.table)
library(ggplot2)
library(ggiraph)
library(scales)
## ── Custom theme ─────────────────────────────────────────────
theme_gst <- function(base_size = 13) {
theme_minimal(base_size = base_size) %+replace%
theme(
plot.background = element_rect(fill = "#0F1923", color = NA),
panel.background = element_rect(fill = "#131F2E", color = NA),
panel.grid.major = element_line(color = "#1E3048", linewidth = 0.4),
panel.grid.minor = element_blank(),
panel.border = element_blank(),
plot.title = element_text(color = "#F0F4F8", size = base_size + 4,
face = "bold", hjust = 0,
margin = margin(b = 4)),
plot.subtitle = element_text(color = "#8FA8C8", size = base_size - 1,
hjust = 0, margin = margin(b = 12)),
plot.caption = element_text(color = "#4A6584", size = 9, hjust = 1),
axis.title = element_text(color = "#8FA8C8", size = base_size - 1),
axis.text = element_text(color = "#7A9AB8", size = base_size - 2),
axis.text.x = element_text(angle = 45, hjust = 1),
strip.text = element_text(color = "#C8DCEF", size = base_size - 1,
face = "bold"),
strip.background = element_rect(fill = "#1A2D44", color = NA),
legend.background = element_rect(fill = "#0F1923", color = NA),
legend.key = element_rect(fill = "#0F1923", color = NA),
legend.text = element_text(color = "#8FA8C8", size = base_size - 2),
legend.title = element_text(color = "#C8DCEF", size = base_size - 1,
face = "bold"),
legend.position = "top",
plot.margin = margin(16, 16, 12, 16)
)
}
gst_colors <- c(
CGST = "#3B9EE0",
SGST = "#2DBE8E",
IGST = "#F5A623",
CESS = "#E05C5C",
TOTAL = "#9B6CF5"
)
## ── Shared girafe options ────────────────────────────────────
girafe_opts <- function(border_color = "#3B9EE0") {
list(
opts_hover(css = "opacity:1;stroke:white;stroke-width:1.5px;"),
opts_hover_inv(css = "opacity:0.2;"),
opts_tooltip(css = paste0(
"background:#1A2D44;color:#F0F4F8;",
"border:1px solid ", border_color, ";border-radius:6px;",
"padding:8px 12px;font-size:13px;font-family:monospace;"
)),
opts_toolbar(saveaspng = TRUE),
opts_sizing(rescale = TRUE)
)
}
fmt_cr <- function(x) {
paste0("₹", formatC(round(x / 1000, 1), format = "f",
digits = 1, big.mark = ","), " K Cr")
}
```
```{r load-data}
#| label: load-data
#| cache: true
gst_data <- fread("/home/pawan/tax/gst.csv")
num_cols <- c("CGST", "SGST", "IGST", "CESS", "TOTAL")
gst_data[, (num_cols) := lapply(.SD, function(x) as.numeric(gsub(",", "", x))),
.SDcols = num_cols]
latest_year <- gst_data[!is.na(TOTAL) & TOTAL > 0, max(Year)]
exclude_rows <- c("Domestic GST Collection- All India",
"Gross Revenue",
"IGST and CESS on Import - All India")
```
## Overview
This interactive report explores **India's Goods and Services Tax (GST)** collection data across states and financial years. All monetary values are expressed in **₹ Thousands Crores (K Cr)**.
> **Tip:** Hover over any chart element to see detailed figures. Use the toolbar icon to download charts as PNG.
---
## 1 · All India Domestic GST Trends {#sec-all-india}
Year-wise breakdown of Central (CGST), State (SGST), Integrated (IGST) and Compensation Cess (CESS) for the consolidated All India basket.
```{r plot1-all-india}
#| label: plot1-all-india
#| fig-cap: "Stacked bars — hover to inspect individual tax components."
all_india_long <- melt(
gst_data[State == "Domestic GST Collection- All India"],
id.vars = c("State", "Year"),
measure.vars = c("CGST", "SGST", "IGST", "CESS"),
variable.name = "Tax_Type",
value.name = "Amount"
)
all_india_long[, tooltip := paste0(
"<b>", Tax_Type, "</b><br>Year: ", Year,
"<br>Amount: ", fmt_cr(Amount)
)]
p1 <- ggplot(all_india_long,
aes(x = Year, y = Amount / 1000,
fill = Tax_Type, group = Tax_Type)) +
geom_bar_interactive(
stat = "identity",
aes(tooltip = tooltip, data_id = paste(Year, Tax_Type)),
width = 0.7, alpha = 0.88
) +
scale_fill_manual(values = gst_colors) +
scale_y_continuous(labels = label_comma(suffix = " K")) +
labs(title = "All India Domestic GST Collection Trends",
subtitle = "Year-wise breakdown by tax type (₹ Thousands Crores)",
x = "Financial Year", y = "Amount (₹ Thousands Crores)",
fill = "Tax Type",
caption = "Source: GST Council / Ministry of Finance") +
theme_gst()
girafe(ggobj = p1, options = girafe_opts("#3B9EE0"))
```
---
## 2 · GST Collection by State {#sec-states}
Top 16 states ranked by cumulative GST, broken down by tax type.
```{r plot2-states}
#| label: plot2-states
#| fig-height: 10
#| fig-cap: "Stacked bars per state — hover to see component amounts."
states_dt <- gst_data[!State %in% exclude_rows]
top16_names <- states_dt[TOTAL > 0,
.(S = sum(TOTAL, na.rm = TRUE)), by = State
][order(-S)][1:16, State]
states_long <- melt(
states_dt[State %in% top16_names],
id.vars = c("State", "Year", "TOTAL"),
measure.vars = c("CGST", "SGST", "IGST", "CESS"),
variable.name = "Tax_Type",
value.name = "Amount"
)
states_long[, tooltip := paste0(
"<b>", State, "</b><br>Year: ", Year,
"<br><i>", Tax_Type, "</i>: ", fmt_cr(Amount),
"<br>Total: ", fmt_cr(TOTAL)
)]
p2 <- ggplot(states_long,
aes(x = Year, y = Amount / 1000, fill = Tax_Type)) +
geom_bar_interactive(
stat = "identity",
aes(tooltip = tooltip, data_id = paste(State, Year, Tax_Type)),
alpha = 0.88
) +
scale_fill_manual(values = gst_colors) +
scale_y_continuous(labels = label_comma()) +
facet_wrap(~State, scales = "free_y", ncol = 4) +
labs(title = "GST Collection by State",
subtitle = "Top 16 states — stacked by tax type (₹ Thousands Crores)",
x = "Financial Year", y = "Amount (₹ Thousands Crores)",
fill = "Tax Type", caption = "Source: GST Council") +
theme_gst() +
theme(axis.text.x = element_text(size = 7))
girafe(ggobj = p2, width_svg = 14, height_svg = 10,
options = girafe_opts("#2DBE8E"))
```
---
## 3 · Major States — Collection Trends {#sec-major}
Line + point chart for 10 major states. Hover a point to see YoY growth.
```{r plot3-major-states}
#| label: plot3-major-states
#| fig-height: 7
#| fig-cap: "Each panel is one state; hover points reveal YoY growth %."
major_states <- c("Maharashtra", "Gujarat", "Tamil Nadu", "Karnataka",
"Uttar Pradesh", "West Bengal", "Delhi",
"Rajasthan", "Madhya Pradesh", "Telangana")
state_palette <- c(
Maharashtra = "#3B9EE0", Gujarat = "#2DBE8E",
`Tamil Nadu` = "#F5A623", Karnataka = "#E05C5C",
`Uttar Pradesh` = "#9B6CF5", `West Bengal` = "#F0C060",
Delhi = "#60C0F0", Rajasthan = "#E08050",
`Madhya Pradesh` = "#70D080", Telangana = "#C070D0"
)
major_dt <- gst_data[State %in% major_states & !is.na(TOTAL) & TOTAL > 0
][order(State, Year)]
major_dt[, Growth := round((TOTAL - shift(TOTAL)) / shift(TOTAL) * 100, 1),
by = State]
major_dt[, tooltip := paste0(
"<b>", State, "</b><br>Year: ", Year,
"<br>Total: ", fmt_cr(TOTAL),
ifelse(!is.na(Growth), paste0("<br>YoY: <b>", Growth, "%</b>"), "")
)]
p3 <- ggplot(major_dt,
aes(x = Year, y = TOTAL / 1000,
color = State, group = State)) +
geom_line_interactive(linewidth = 1.3,
aes(tooltip = State, data_id = State)) +
geom_point_interactive(size = 2.5,
aes(tooltip = tooltip,
data_id = paste(State, Year))) +
scale_color_manual(values = state_palette) +
scale_y_continuous(labels = label_comma()) +
facet_wrap(~State, ncol = 5, scales = "free_y") +
labs(title = "GST Collection Trends — Major States",
subtitle = "Hover a point to see YoY growth (₹ Thousands Crores)",
x = "Financial Year", y = "Total GST (₹ Thousands Crores)",
caption = "Source: GST Council") +
theme_gst() +
theme(legend.position = "none")
girafe(ggobj = p3, width_svg = 14, height_svg = 7,
options = girafe_opts("#F5A623"))
```
---
## 4 · GST Composition — `r latest_year` {#sec-composition}
Donut showing the share of each tax head for the latest available financial year across all states.
```{r plot4-composition}
#| label: plot4-composition
#| fig-height: 5
#| fig-cap: "Hover a segment to see the exact amount and share."
latest_comp <- gst_data[!State %in% exclude_rows & Year == latest_year,
.(CGST = sum(CGST, na.rm = TRUE),
SGST = sum(SGST, na.rm = TRUE),
IGST = sum(IGST, na.rm = TRUE),
CESS = sum(CESS, na.rm = TRUE))]
comp_long <- melt(latest_comp,
measure.vars = c("CGST", "SGST", "IGST", "CESS"),
variable.name = "Tax_Type",
value.name = "Amount")
comp_long[, Share := round(Amount / sum(Amount) * 100, 1)]
comp_long[, tooltip := paste0(
"<b>", Tax_Type, "</b><br>",
fmt_cr(Amount), "<br>Share: <b>", Share, "%</b>"
)]
p4 <- ggplot(comp_long,
aes(x = "", y = Amount / 1000, fill = Tax_Type)) +
geom_bar_interactive(
stat = "identity", width = 1,
aes(tooltip = tooltip, data_id = as.character(Tax_Type)),
color = "#0F1923", linewidth = 1.2
) +
coord_polar("y", start = 0) +
geom_text(aes(label = paste0(Tax_Type, "\n", Share, "%")),
position = position_stack(vjust = 0.55),
color = "white", size = 4.2, fontface = "bold") +
scale_fill_manual(values = gst_colors) +
labs(title = paste0("GST Composition — All States (", latest_year, ")"),
subtitle = "Distribution across tax types",
fill = "Tax Type", caption = "Source: GST Council") +
theme_gst() +
theme(axis.title = element_blank(), axis.text = element_blank(),
panel.grid = element_blank())
girafe(ggobj = p4, options = girafe_opts("#9B6CF5"))
```
---
## 5 · Heatmap — State × Year {#sec-heatmap}
```{r plot5-heatmap}
#| label: plot5-heatmap
#| fig-height: 7
#| fig-cap: "Top 20 states by cumulative GST; brighter = higher collection."
heatmap_dt <- gst_data[
!State %in% c(exclude_rows, "OIDAR", "Other Territory") & TOTAL > 0,
.(State, Year, TOTAL)
]
top20 <- heatmap_dt[, .(S = sum(TOTAL, na.rm = TRUE)), by = State
][order(-S)][1:20, State]
heatmap_dt <- heatmap_dt[State %in% top20]
heatmap_dt[, tooltip := paste0(
"<b>", State, "</b><br>Year: ", Year, "<br>Total: ", fmt_cr(TOTAL)
)]
p5 <- ggplot(heatmap_dt,
aes(x = Year,
y = reorder(State, TOTAL, FUN = sum),
fill = TOTAL / 1000)) +
geom_tile_interactive(
aes(tooltip = tooltip, data_id = paste(State, Year)),
color = "#0F1923", linewidth = 0.4
) +
scale_fill_gradient2(
low = "#0D2137", mid = "#1E6FAD", high = "#FFD166",
midpoint = median(heatmap_dt$TOTAL / 1000, na.rm = TRUE),
name = "₹ K Crores", labels = label_comma()
) +
labs(title = "Heatmap — GST Collections by State & Year",
subtitle = "Top 20 States/UTs (₹ Thousands Crores)",
x = "Financial Year", y = NULL,
caption = "Source: GST Council") +
theme_gst() +
theme(legend.position = "right",
axis.text.y = element_text(size = 9))
girafe(ggobj = p5, width_svg = 12, height_svg = 7,
options = girafe_opts("#FFD166"))
```
---
## 6 · YoY Growth Rate — All India {#sec-growth}
```{r plot6-growth}
#| label: plot6-growth
#| fig-cap: "Green = positive growth; red = contraction."
growth_dt <- gst_data[
State == "Domestic GST Collection- All India" & !is.na(TOTAL) & TOTAL > 0
][order(Year)]
growth_dt[, Growth_Rate := round((TOTAL - shift(TOTAL)) / shift(TOTAL) * 100, 1)]
growth_dt[, tooltip := paste0(
"<b>", Year, "</b><br>Growth: <b>",
ifelse(!is.na(Growth_Rate), paste0(Growth_Rate, "%"), "N/A"),
"</b><br>Total: ", fmt_cr(TOTAL)
)]
growth_dt[, bar_fill := ifelse(Growth_Rate >= 0, "Positive", "Negative")]
p6 <- ggplot(growth_dt[!is.na(Growth_Rate)],
aes(x = Year, y = Growth_Rate, fill = bar_fill)) +
geom_bar_interactive(
stat = "identity", width = 0.65, alpha = 0.88,
aes(tooltip = tooltip, data_id = Year)
) +
geom_hline(yintercept = 0, color = "#8FA8C8", linewidth = 0.6) +
geom_text(
aes(label = paste0(Growth_Rate, "%"),
vjust = ifelse(Growth_Rate >= 0, -0.4, 1.4)),
color = "#C8DCEF", size = 3.5, fontface = "bold"
) +
scale_fill_manual(values = c(Positive = "#2DBE8E", Negative = "#E05C5C"),
guide = "none") +
scale_y_continuous(labels = label_percent(scale = 1)) +
labs(title = "All India Domestic GST — YoY Growth Rate",
subtitle = "Percentage change from previous financial year",
x = "Financial Year", y = "Growth Rate (%)",
caption = "Source: GST Council") +
theme_gst()
girafe(ggobj = p6, options = girafe_opts("#2DBE8E"))
```
---
## 7 · CGST vs SGST — Selected States {#sec-cgst-sgst}
```{r plot7-cgst-sgst}
#| label: plot7-cgst-sgst
#| fig-height: 7
#| fig-cap: "Dodged bars — CGST and SGST compared side by side."
comp_data <- gst_data[State %in% c("Maharashtra", "Gujarat",
"Tamil Nadu", "Karnataka") &
Year >= "2019-20"]
comp_long2 <- melt(comp_data,
id.vars = c("State", "Year"),
measure.vars = c("CGST", "SGST"),
variable.name = "Tax_Type",
value.name = "Amount")
comp_long2[, tooltip := paste0(
"<b>", State, "</b><br>Year: ", Year,
"<br>", Tax_Type, ": ", fmt_cr(Amount)
)]
p7 <- ggplot(comp_long2,
aes(x = Year, y = Amount / 1000, fill = Tax_Type)) +
geom_bar_interactive(
stat = "identity", position = "dodge", width = 0.7, alpha = 0.88,
aes(tooltip = tooltip, data_id = paste(State, Year, Tax_Type))
) +
scale_fill_manual(values = c(CGST = "#3B9EE0", SGST = "#2DBE8E")) +
scale_y_continuous(labels = label_comma()) +
facet_wrap(~State, scales = "free_y") +
labs(title = "CGST vs SGST — Major States",
subtitle = "Central vs State GST since 2019-20 (₹ Thousands Crores)",
x = "Financial Year", y = "Amount (₹ Thousands Crores)",
fill = "Tax Type", caption = "Source: GST Council") +
theme_gst()
girafe(ggobj = p7, width_svg = 11, height_svg = 7,
options = girafe_opts("#3B9EE0"))
```
---
## 8 · CESS Contribution Trend {#sec-cess}
```{r plot8-cess}
#| label: plot8-cess
#| fig-cap: "Hover points to see exact CESS share per year."
cess_dt <- gst_data[!State %in% exclude_rows & TOTAL > 0,
.(CESS_Share = round(
sum(CESS, na.rm = TRUE) /
sum(TOTAL, na.rm = TRUE) * 100, 2)),
by = Year][order(Year)]
cess_dt[, tooltip := paste0(
"<b>", Year, "</b><br>CESS Share: <b>", CESS_Share, "%</b>"
)]
p8 <- ggplot(cess_dt, aes(x = Year, y = CESS_Share, group = 1)) +
geom_line_interactive(
color = "#F5A623", linewidth = 1.4,
aes(tooltip = "CESS trend", data_id = "line")
) +
geom_point_interactive(
color = "#E05C5C", size = 4, alpha = 0.9,
aes(tooltip = tooltip, data_id = Year)
) +
geom_text(aes(label = paste0(CESS_Share, "%")),
vjust = -1, color = "#C8DCEF", size = 3.5) +
scale_y_continuous(
labels = label_percent(scale = 1),
limits = c(0, max(cess_dt$CESS_Share, na.rm = TRUE) * 1.3)
) +
labs(title = "CESS Contribution to Total GST",
subtitle = "Percentage share of CESS across all states, by year",
x = "Financial Year", y = "CESS Share (%)",
caption = "Source: GST Council") +
theme_gst()
girafe(ggobj = p8, options = girafe_opts("#F5A623"))
```
---
## 9 · Statistical Summary Tables {#sec-tables}
### Top 15 States by Average GST Collection
```{r table-avg}
#| label: table-avg
state_avg <- gst_data[
!State %in% c(exclude_rows, "OIDAR", "Other Territory") & TOTAL > 0,
.(
`Avg (₹ K Cr)` = round(mean(TOTAL, na.rm = TRUE) / 1000, 2),
`Max (₹ K Cr)` = round(max(TOTAL, na.rm = TRUE) / 1000, 2),
`Min (₹ K Cr)` = round(min(TOTAL, na.rm = TRUE) / 1000, 2)
),
by = State
][order(-`Avg (₹ K Cr)`)][1:15]
knitr::kable(state_avg, format.args = list(big.mark = ","))
```
### Top 10 Growth Leaders (2023-24 → 2024-25)
```{r table-growth}
#| label: table-growth
growth_by_state <- gst_data[
!State %in% c(exclude_rows, "OIDAR", "Other Territory") &
Year %in% c("2023-24", "2024-25") & TOTAL > 0
]
growth_wide <- dcast(growth_by_state, State ~ Year, value.var = "TOTAL")
growth_wide[, `Growth (%)` := round((`2024-25` - `2023-24`) / `2023-24` * 100, 2)]
knitr::kable(
growth_wide[order(-`Growth (%)`)][1:10,
.(State, `2023-24 (₹ Cr)` = `2023-24`,
`2024-25 (₹ Cr)` = `2024-25`,
`Growth (%)`)],
format.args = list(big.mark = ",")
)
```
---
## Session Info {#sec-session}
```{r session}
#| label: session
#| code-fold: true
#| code-summary: "R session info"
sessionInfo()
```