library(ggplot2)
## Warning: package 'ggplot2' was built under R version 4.5.3
set.seed(123)

eps    <- 0.78
minPts <- 4

#pusat cluster
centers <- data.frame(
  cluster = c("A", "C", "B", "D"),
  x = c(2.6, 5.9, 2.7, 5.3),
  y = c(5.2, 5.2, 2.0, 2.0)
)

#core point
generate_cluster <- function(cx, cy, n = 28, radius = 0.48) {
  angle    <- runif(n, 0, 2 * pi)
  distance <- sqrt(runif(n)) * radius
  data.frame(
    x = cx + distance * cos(angle),
    y = cy + distance * sin(angle)
  )
}

core_points <- do.call(
  rbind,
  lapply(1:nrow(centers), function(i) {
    d <- generate_cluster(centers$x[i], centers$y[i])
    d$cluster <- centers$cluster[i]
    d
  })
)

#border point
sudut_tepi <- c(20, 110, 200, 290)   # derajat

cari_titik_tepi <- function(cx, cy, derajat) {
  a <- derajat * pi / 180
  for (r in seq(0.30, 3.0, by = 0.002)) {
    px <- cx + r * cos(a)
    py <- cy + r * sin(a)
    n_core <- sum(sqrt((core_points$x - px)^2 + (core_points$y - py)^2) <= eps)
    if (n_core <= minPts - 2) return(c(px, py))
  }
  stop("Titik tepi tidak ditemukan, ubah sudut atau parameter.")
}

kandidat_tepi <- do.call(
  rbind,
  lapply(1:nrow(centers), function(i) {
    do.call(rbind, lapply(sudut_tepi, function(s) {
      p <- cari_titik_tepi(centers$x[i], centers$y[i], s)
      data.frame(x = p[1], y = p[2], cluster = centers$cluster[i])
    }))
  })
)

#noise
kandidat_noise <- data.frame(
  x = c(0.8, 7.0, 6.5),
  y = c(6.0, 3.2, 1.0),
  cluster = NA_character_
)

#klasifikasi semua titik
semua <- rbind(
  data.frame(core_points,    asal = "core"),
  data.frame(kandidat_tepi,  asal = "kandidat_border"),
  data.frame(kandidat_noise, asal = "kandidat_noise")
)

D        <- as.matrix(dist(semua[, c("x", "y")]))
semua$tetangga <- rowSums(D <= eps)          # termasuk titik itu sendiri
is_core  <- semua$tetangga >= minPts
idx_core <- which(is_core)

semua$jarak_core <- sapply(seq_len(nrow(semua)), function(i)
  if (is_core[i]) 0 else min(D[i, idx_core]))

semua$jenis <- ifelse(is_core, "Core",
                      ifelse(semua$jarak_core <= eps, "Border", "Noise"))

# Pemeriksaan: hasil klasifikasi harus sesuai maksud tiap kelompok titik
stopifnot(
  "Semua titik asal 'core' harus berjenis Core" =
    all(semua$jenis[semua$asal == "core"] == "Core"),
  "Semua kandidat border harus berjenis Border" =
    all(semua$jenis[semua$asal == "kandidat_border"] == "Border"),
  "Semua kandidat noise harus berjenis Noise" =
    all(semua$jenis[semua$asal == "kandidat_noise"] == "Noise")
)

cat("Jumlah titik per jenis:\n")
## Jumlah titik per jenis:
print(table(semua$jenis))
## 
## Border   Core  Noise 
##     16    112      3
cat("\nDaftar titik non-core (border dan noise):\n")
## 
## Daftar titik non-core (border dan noise):
tabel_nc <- semua[semua$jenis != "Core",
                  c("x", "y", "cluster", "jenis", "tetangga", "jarak_core")]
tabel_nc$x <- round(tabel_nc$x, 3)
tabel_nc$y <- round(tabel_nc$y, 3)
tabel_nc$jarak_core <- round(tabel_nc$jarak_core, 3)
print(tabel_nc, row.names = FALSE)
##      x     y cluster  jenis tetangga jarak_core
##  3.604 5.565       A Border        3      0.641
##  2.255 6.149       A Border        3      0.696
##  1.578 4.828       A Border        3      0.670
##  2.958 4.217       A Border        3      0.677
##  6.815 5.533       C Border        3      0.635
##  5.504 6.288       C Border        3      0.730
##  4.893 4.833       C Border        3      0.667
##  6.271 4.179       C Border        3      0.663
##  3.705 2.366       B Border        2      0.728
##  2.340 2.989       B Border        3      0.610
##  1.698 1.635       B Border        3      0.626
##  3.053 1.030       B Border        3      0.685
##  6.228 2.338       D Border        3      0.740
##  4.906 3.083       D Border        3      0.696
##  4.189 1.596       D Border        3      0.741
##  5.652 1.032       D Border        3      0.615
##  0.800 6.000    <NA>  Noise        1      1.506
##  7.000 3.200    <NA>  Noise        1      1.779
##  6.500 1.000    <NA>  Noise        1      1.177
cat("\nJarak border terjauh ke core terdekat:",
    round(max(semua$jarak_core[semua$jenis == "Border"]), 3), "(eps =", eps, ")\n")
## 
## Jarak border terjauh ke core terdekat: 0.741 (eps = 0.78 )
cat("Jarak noise terdekat ke core terdekat :",
    round(min(semua$jarak_core[semua$jenis == "Noise"]), 3), "(eps =", eps, ")\n")
## Jarak noise terdekat ke core terdekat : 1.177 (eps = 0.78 )
core_df   <- semua[which(semua$jenis == "Core"), ]
border_df <- semua[which(semua$jenis == "Border"), ]
noise_df  <- semua[which(semua$jenis == "Noise"), ]

#Lingkaran epsilon di sekitar core point yang menjangkau tiap border

create_circle <- function(cx, cy, radius = eps, id = "A") {
  angle <- seq(0, 2 * pi, length.out = 300)
  data.frame(x = cx + radius * cos(angle),
             y = cy + radius * sin(angle),
             group = id)
}

idx_border <- which(semua$jenis == "Border")
anchor     <- sapply(idx_border, function(i) idx_core[which.min(D[i, idx_core])])
anchor_unik <- unique(anchor)

# Core yang paling dekat dengan pusat tiap cluster: dipakai untuk panah epsilon
pusat_core <- sapply(seq_len(nrow(centers)), function(i) {
  kandidat <- idx_core[which(semua$cluster[idx_core] == centers$cluster[i])]
  kandidat[which.min(sqrt((semua$x[kandidat] - centers$x[i])^2 +
                            (semua$y[kandidat] - centers$y[i])^2))]
})
lingkaran_idx <- unique(c(anchor_unik, pusat_core))

circles <- do.call(rbind, lapply(seq_along(lingkaran_idx), function(k)
  create_circle(semua$x[lingkaran_idx[k]], semua$y[lingkaran_idx[k]],
                id = paste0("c", k))))

# Panah epsilon: dari core pusat tiap cluster ke kanan, panjang = eps
panah <- data.frame(x = semua$x[pusat_core], y = semua$y[pusat_core])
panah$xend <- panah$x + eps

# Titik acuan untuk anotasi (diambil dari data, bukan koordinat manual)
b_A  <- border_df[which(border_df$cluster == "A"), ][1, ]
b_C2 <- border_df[which(border_df$cluster == "C"), ][2, ]
b_Cr <- border_df[which(border_df$cluster == "C"), ]
b_Cr <- b_Cr[which.max(b_Cr$x), ]
b_B2 <- border_df[which(border_df$cluster == "B"), ][2, ]
b_B3 <- border_df[which(border_df$cluster == "B"), ][3, ]
core_C <- core_df[which(core_df$cluster == "C"), ]
core_Cr <- core_C[which.max(core_C$x), ]

#visualisasi
panah_tipe <- grid::arrow(length = grid::unit(0.13, "inches"))

p <- ggplot() +
  
  # Lingkaran epsilon
  geom_path(
    data = circles,
    aes(x = x, y = y, group = group),
    color = "grey65", linetype = "dashed", linewidth = 0.45
  ) +
  
  # Core points
  geom_point(data = core_df, aes(x, y), shape = 21, size = 2.8,
             fill = "#3986C6", color = "#245F99", stroke = 0.3) +
  
  # Border points
  geom_point(data = border_df, aes(x, y), shape = 21, size = 2.8,
             fill = "#B5D98B", color = "#7EAD5B", stroke = 0.4) +
  
  # Noise points
  geom_point(data = noise_df, aes(x, y), shape = 21, size = 3.3,
             fill = "#F28C36", color = "#B96C24", stroke = 0.4) +
  
  # Label cluster (nama unik)
  annotate("text", x = 2.6, y = 3.7,  label = "Cluster A", size = 4.2) +
  annotate("text", x = 5.9, y = 3.7,  label = "Cluster C", size = 4.2) +
  annotate("text", x = 2.7, y = 0.65, label = "Cluster B", size = 4.2) +
  annotate("text", x = 5.3, y = 0.65, label = "Cluster D", size = 4.2) +
  
  # CORE POINTS
  annotate("text", x = 3.0, y = 6.75, label = "CORE POINTS", size = 5.2) +
  annotate("segment", x = 3.55, y = 6.5, xend = 2.85, yend = 5.55,
           arrow = panah_tipe, linewidth = 0.7) +
  annotate("segment", x = 3.7, y = 6.5, xend = 5.62, yend = 5.6,
           arrow = panah_tipe, linewidth = 0.7) +
  
  # BORDER POINTS (atas): panah ke border sungguhan cluster C
  annotate("text", x = 4.8, y = 6.95, label = "BORDER POINTS", size = 5) +
  annotate("segment", x = 5.15, y = 6.75,
           xend = b_C2$x - 0.05, yend = b_C2$y + 0.06,
           arrow = panah_tipe, linewidth = 0.7) +
  
  # BORDER POINTS (kiri bawah): panah ke border sungguhan cluster B
  annotate("text", x = 0.85, y = 3.55, label = "BORDER\nPOINTS", size = 4.5) +
  annotate("segment", x = 1.5, y = 3.4,
           xend = b_B2$x - 0.06, yend = b_B2$y + 0.06,
           arrow = panah_tipe, linewidth = 0.7) +
  
  # Penjelasan border point (atas)
  annotate("text", x = 7.25, y = 5.95,
           label = "Border Point (Green) within\nCore Point's radius",
           hjust = 0, size = 4.1) +
  annotate("segment", x = 7.15, y = 5.8,
           xend = b_Cr$x + 0.08, yend = b_Cr$y + 0.02,
           arrow = panah_tipe, linewidth = 0.7) +
  
  # Penjelasan core point (bawah)
  annotate("text", x = 7.25, y = 4.55,
           label = "Core Point (Blue) density radius\ncontains MinPts points",
           hjust = 0, size = 4.1) +
  annotate("segment", x = 7.15, y = 4.7,
           xend = core_Cr$x + 0.07, yend = core_Cr$y - 0.02,
           arrow = panah_tipe, linewidth = 0.7) +
  
  # Radius epsilon (panjang panah = eps)
  geom_segment(data = panah,
               aes(x = x, y = y, xend = xend, yend = y),
               arrow = grid::arrow(length = grid::unit(0.15, "inches"),
                                   type = "closed"),
               linewidth = 0.7) +
  geom_text(data = panah, aes(x = x + eps / 2, y = y + 0.22),
            label = "\u03b5", size = 5) +
  
  # Contoh titik border
  annotate("text", x = b_A$x + 0.12, y = b_A$y + 0.2,
           label = "e.g., border point\nin cluster A", hjust = 0, size = 3.5) +
  annotate("text", x = b_B3$x, y = b_B3$y - 0.33,
           label = "e.g., border point\nin cluster B", size = 3.5) +
  
  # Label noise
  annotate("text", x = 0.9, y = 6.2, label = "Point N1", hjust = 0, size = 3.5) +
  annotate("text", x = 0.9, y = 5.8,
           label = "outside every core\npoint's radius = noise",
           hjust = 0, size = 3.1, color = "grey30") +
  annotate("text", x = 7.1, y = 3.0, label = "Point N2", hjust = 0, size = 3.5) +
  annotate("text", x = 6.5, y = 1.4, label = "NOISE (OUTLIERS)",
           size = 4) +
  
  # Informasi parameter
  annotate("text", x = 8.1, y = 2.65,
           label = paste0("MinPts = ", minPts, "\nRadius = \u03b5"),
           hjust = 0, size = 4.5) +
  
  # Legenda
  annotate("rect", xmin = 7.35, xmax = 9.9, ymin = 0.55, ymax = 2.15,
           fill = "white", color = NA) +
  annotate("point", x = 7.55, y = 1.85, shape = 21, size = 4,
           fill = "#3986C6", color = "#245F99") +
  annotate("text", x = 7.75, y = 1.85, label = "Core Point (Blue)",
           hjust = 0, size = 4) +
  annotate("point", x = 7.55, y = 1.35, shape = 21, size = 4,
           fill = "#B5D98B", color = "#7EAD5B") +
  annotate("text", x = 7.75, y = 1.35, label = "Border Point (Light Green)",
           hjust = 0, size = 4) +
  annotate("point", x = 7.55, y = 0.85, shape = 21, size = 4,
           fill = "#F28C36", color = "#B96C24") +
  annotate("text", x = 7.75, y = 0.85, label = "Noise/Outlier (Orange)",
           hjust = 0, size = 4) +
  
  # Sumbu koordinat
  annotate("segment", x = 0.35, y = 0.35, xend = 9.5, yend = 0.35,
           linewidth = 0.8,
           arrow = grid::arrow(length = grid::unit(0.16, "inches"), type = "closed")) +
  annotate("segment", x = 0.35, y = 0.35, xend = 0.35, yend = 7.1,
           linewidth = 0.8,
           arrow = grid::arrow(length = grid::unit(0.16, "inches"), type = "closed")) +
  annotate("text", x = 9.35, y = 0.12, label = "X", size = 5) +
  annotate("text", x = 0.15, y = 7.05, label = "Y", size = 5) +
  
  labs(title = "DBSCAN CLUSTERING: KEY CONCEPTS") +
  
  # ratio = 1 agar lingkaran epsilon benar-benar bulat
  coord_fixed(ratio = 1, xlim = c(0, 10), ylim = c(0, 7.3),
              expand = FALSE, clip = "off") +
  
  theme_void(base_size = 14) +
  theme(
    plot.title = element_text(size = 21, face = "bold", hjust = 0.5,
                              color = "black", margin = margin(b = 18)),
    plot.background = element_rect(fill = "white", color = NA),
    plot.margin = margin(15, 15, 15, 15)
  )

print(p)

ggsave("DBSCAN_Clustering_Key_Concepts.png", plot = p,
       width = 13, height = 8.5, dpi = 300, bg = "white")
ggsave("DBSCAN_Clustering_Key_Concepts.pdf", plot = p,
       width = 13, height = 8.5, bg = "white", device = cairo_pdf)