import matplotlib.pyplot as plt
import numpy as np
from io import StringIO
from matplotlib.patches import Circle
from sklearn.cluster import DBSCAN

# Ten points, eps = 1.5, min_samples = 4 (the point itself counts).
# A circle of radius eps is drawn around three points: a core point (D),
# a border point (E) and a noise point (F).
names = list('ABCDEFGHIJ')
X = np.array([[1, 1], [2, 1], [1, 2], [2, 2], [3, 3], [4, 4],
              [6, 1], [7, 1], [6, 2], [7, 2]], float)
db = DBSCAN(eps=1.5, min_samples=4).fit(X)
core = np.zeros(len(X), bool)
core[db.core_sample_indices_] = True
kind = np.where(core, 'core', np.where(db.labels_ >= 0, 'border', 'noise'))

BLUE, ORANGE, RED = '#58a6ff', '#f0883e', '#ff7b72'
style = {'core': (BLUE, 'o', 90), 'border': (ORANGE, 's', 80), 'noise': (RED, 'X', 110)}
plt.rcParams.update({'text.color': 'gray', 'axes.labelcolor': 'gray', 'axes.edgecolor': 'gray',
                     'xtick.color': 'gray', 'ytick.color': 'gray'})
fig, ax = plt.subplots(figsize=(7, 3.9))
for i in [3, 4, 5]:
    c = style[kind[i]][0]
    ax.add_patch(Circle(X[i], 1.5, facecolor=c, alpha=0.08, lw=0))
    ax.add_patch(Circle(X[i], 1.5, facecolor='none', edgecolor=c, alpha=0.6, ls='--', lw=1.0))
for k, (c, m, s) in style.items():
    sel = kind == k
    ax.scatter(X[sel, 0], X[sel, 1], color=c, marker=m, s=s, label=f'{k} ({sel.sum()})', zorder=3)
for i, p in enumerate(X):
    ax.annotate(f'{names[i]}', p, textcoords='offset points', xytext=(-12, 6), fontsize=9)
for j in range(db.labels_.max() + 1):
    ctr = X[db.labels_ == j].mean(axis=0)
    ax.text(ctr[0] + 0.2, -0.05, f'cluster {j}', ha='center', fontsize=9, color='gray')
ax.text(4.25, 4.55, 'F sees only E, and E is not core:\nF is noise, although it touches a cluster',
        fontsize=8, color=RED)
ax.text(3.3, 2.35, 'E reaches core D → border', fontsize=8, color=ORANGE)
ax.set_xlim(-0.3, 8.8)
ax.set_ylim(-0.4, 5.6)
ax.set_aspect('equal')
ax.grid(alpha=0.2)
ax.tick_params(labelsize=8)
ax.legend(fontsize=8.5, frameon=False, loc='upper left')
ax.set_title('DBSCAN by hand: eps = 1.5, min_samples = 4 (circles of radius eps around D, E, F)',
             fontsize=9.5)
plt.tight_layout()

buffer = StringIO()
plt.savefig(buffer, format="svg", transparent=True)
print(buffer.getvalue())

plt.close()
