sillyvis 0.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ # Build artifacts
2
+ dist/
3
+ build/
4
+ *.egg-info/
5
+ *.whl
6
+ *.tar.gz
7
+
8
+ # Python
9
+ __pycache__/
10
+ *.pyc
11
+ *.pyo
12
+ .pytest_cache/
13
+ .ruff_cache/
14
+
15
+ # IDE
16
+ .vscode/
17
+ .idea/
18
+
19
+ # uv
20
+ .venv/
21
+ uv.lock
@@ -0,0 +1,7 @@
1
+ Metadata-Version: 2.5
2
+ Name: sillyvis
3
+ Version: 0.1.1
4
+ Summary: Sorting algorithm visualizer powered by matplotlib animations
5
+ Author-email: Small Ai <3910271726@qq.com>
6
+ Requires-Python: >=3.8
7
+ Requires-Dist: matplotlib>=3.5
@@ -0,0 +1,59 @@
1
+ # sillysort
2
+
3
+ > 一行代码,把排序算法变成动画。
4
+
5
+ `sillysort` 是一个用 matplotlib 驱动的排序算法可视化库。你只需要写一个生成器函数描述排序过程,剩下的交给 `go`。
6
+
7
+ ## 安装
8
+
9
+ ```bash
10
+ pip install sillysort
11
+ ```
12
+
13
+ ## 快速上手
14
+
15
+ ```python
16
+ from sillysort import Silly as s, frame
17
+
18
+ def bubble_sort(arr):
19
+ n = len(arr)
20
+ for i in range(n):
21
+ for j in range(n - i - 1):
22
+ if arr[j] > arr[j + 1]:
23
+ arr[j], arr[j + 1] = arr[j + 1], arr[j]
24
+ yield frame(
25
+ (arr, {j: s.LEFT_PTR, j + 1: s.RIGHT_PTR}),
26
+ text=f"比较并交换 ({i+1}/{n})"
27
+ )
28
+
29
+ s([3, 1, 4, 2, 7, 5, 6]).go(bubble_sort)
30
+ ```
31
+
32
+ ## 高亮颜色
33
+
34
+ | 常量 | 含义 | 颜色 |
35
+ |---|---|---|
36
+ | `Silly.NORMAL` | 默认 | 蓝色 |
37
+ | `Silly.HIGHLIGHT` | 高亮 | 红色 |
38
+ | `Silly.LEFT_PTR` | 左指针 | 橙色 |
39
+ | `Silly.RIGHT_PTR` | 右指针 | 紫色 |
40
+ | `Silly.LOCKED` | 已锁定 | 绿色 |
41
+
42
+ ## 多排模式
43
+
44
+ 同时展示主数组 + 辅助数组:
45
+
46
+ ```python
47
+ def merge_sort(arr):
48
+ aux = [0] * len(arr)
49
+ # ... 排序逻辑
50
+ yield frame(
51
+ (arr, {i: s.HIGHLIGHT}),
52
+ (aux, {k: s.LOCKED}),
53
+ text="归并中"
54
+ )
55
+ ```
56
+
57
+ ## 许可
58
+
59
+ MIT
@@ -0,0 +1,29 @@
1
+ """冒泡排序示例"""
2
+
3
+ from sillysort import Silly as s, frame
4
+
5
+
6
+ def bubble_sort(arr):
7
+ n = len(arr)
8
+ for i in range(n):
9
+ for j in range(n - i - 1):
10
+ yield frame(
11
+ (arr, {j: s.LEFT_PTR, j + 1: s.RIGHT_PTR}),
12
+ text=f"第 {i+1} 轮比较",
13
+ )
14
+ if arr[j] > arr[j + 1]:
15
+ arr[j], arr[j + 1] = arr[j + 1], arr[j]
16
+ yield frame(
17
+ (arr, {j: s.HIGHLIGHT, j + 1: s.HIGHLIGHT}),
18
+ text=f"交换 {arr[j+1]} <-> {arr[j]}",
19
+ )
20
+
21
+ # 全部锁定
22
+ yield frame((arr, {i: s.LOCKED for i in range(n)}), text="排序完成!")
23
+
24
+
25
+ if __name__ == "__main__":
26
+ import random
27
+
28
+ data = random.sample(range(1, 50), 15)
29
+ s(data).go(bubble_sort)
@@ -0,0 +1,58 @@
1
+ """归并排序 —— 多排模式展示原数组 + 辅助数组"""
2
+
3
+ from sillysort import Silly as s, frame
4
+
5
+
6
+ def merge_sort(arr):
7
+ aux = [0] * len(arr)
8
+
9
+ def _merge(lo, mid, hi):
10
+ aux[lo : hi + 1] = arr[lo : hi + 1]
11
+ yield frame(
12
+ (arr, {i: s.NORMAL for i in range(len(arr))}),
13
+ (aux, {i: s.HIGHLIGHT for i in range(lo, hi + 1)}),
14
+ text=f"拷贝 [{lo}..{hi}] 到辅助数组",
15
+ )
16
+
17
+ i, j = lo, mid + 1
18
+ for k in range(lo, hi + 1):
19
+ if i > mid:
20
+ arr[k] = aux[j]
21
+ j += 1
22
+ elif j > hi:
23
+ arr[k] = aux[i]
24
+ i += 1
25
+ elif aux[j] < aux[i]:
26
+ arr[k] = aux[j]
27
+ j += 1
28
+ else:
29
+ arr[k] = aux[i]
30
+ i += 1
31
+
32
+ yield frame(
33
+ (arr, {k: s.HIGHLIGHT}),
34
+ (aux, {lo: s.LEFT_PTR, hi: s.RIGHT_PTR}),
35
+ text=f"归并写入位置 {k}",
36
+ )
37
+
38
+ def _sort(lo, hi):
39
+ if hi <= lo:
40
+ return
41
+ mid = (lo + hi) // 2
42
+ yield from _sort(lo, mid)
43
+ yield from _sort(mid + 1, hi)
44
+ yield from _merge(lo, mid, hi)
45
+
46
+ yield from _sort(0, len(arr) - 1)
47
+ yield frame(
48
+ (arr, {i: s.LOCKED for i in range(len(arr))}),
49
+ (aux, {}),
50
+ text="归并排序完成!",
51
+ )
52
+
53
+
54
+ if __name__ == "__main__":
55
+ import random
56
+
57
+ data = random.sample(range(1, 50), 12)
58
+ s(data).go(merge_sort)
@@ -0,0 +1,15 @@
1
+ [project]
2
+ name = "sillyvis"
3
+ version = "0.1.1"
4
+ description = "Sorting algorithm visualizer powered by matplotlib animations"
5
+ authors = [{name = "Small Ai", email = "3910271726@qq.com"}]
6
+ dependencies = ["matplotlib>=3.5"]
7
+ requires-python = ">=3.8"
8
+
9
+ [build-system]
10
+ requires = ["hatchling"]
11
+ build-backend = "hatchling.build"
12
+
13
+ [tool.hatch.build.targets.wheel]
14
+ packages = ["src/sillyvis"]
15
+
@@ -0,0 +1,6 @@
1
+ """sillysort — 排序算法可视化"""
2
+
3
+ from .core import Silly, frame
4
+
5
+ __all__ = ["Silly", "frame"]
6
+ __version__ = "0.1.0"
@@ -0,0 +1,170 @@
1
+ import matplotlib
2
+
3
+ matplotlib.use("TkAgg")
4
+
5
+ import matplotlib.pyplot as plt
6
+ from matplotlib.animation import FuncAnimation
7
+ from matplotlib import rcParams, font_manager
8
+
9
+
10
+ class Silly:
11
+ """排序可视化器
12
+
13
+ 约定(多排模式):
14
+ yield ( [arr, aux, count, ...], # list of list,每排一个数组
15
+ [hl1, hl2, hl3, ...], # 同长度的 list of dict,每排各自的高亮
16
+ "描述文字" )
17
+ hl 每个元素可以是 dict {idx: color},也可以是一组索引 (用默认高亮色)。
18
+
19
+ 单排模式(兼容旧用法):
20
+ yield ( arr, hl_dict, "描述文字" )
21
+ """
22
+
23
+ NORMAL = "#4e8df5"
24
+ HIGHLIGHT = "#ff4d4d"
25
+ LEFT_PTR = "#ff9f1c"
26
+ RIGHT_PTR = "#9b5de5"
27
+ LOCKED = "#2ecc71"
28
+
29
+ def __init__(self, data, n=None, font_path=None):
30
+ self.data = list(data)
31
+ self.n = len(self.data) if n is None else n
32
+
33
+ # 中文字体
34
+ if font_path:
35
+ try:
36
+ font_manager.fontManager.addfont(font_path)
37
+ except Exception:
38
+ pass
39
+ available = [
40
+ f.name
41
+ for f in font_manager.fontManager.ttflist
42
+ if "Noto" in f.name and "CJK" in f.name
43
+ ]
44
+ cjk_name = None
45
+ for name in available:
46
+ if "SC" in name:
47
+ cjk_name = name
48
+ break
49
+ if cjk_name is None and available:
50
+ cjk_name = available[0]
51
+ if cjk_name:
52
+ rcParams["font.sans-serif"] = [cjk_name]
53
+ rcParams["font.family"] = "sans-serif"
54
+ rcParams["axes.unicode_minus"] = False
55
+
56
+ self.fig, self.ax = plt.subplots(figsize=(13, 7))
57
+ self.ax.set_xticks([])
58
+ self.ax.set_yticks([])
59
+ self.info_text = self.ax.text(
60
+ 0.01,
61
+ 0.99,
62
+ "",
63
+ transform=self.ax.transAxes,
64
+ fontsize=13,
65
+ fontweight="bold",
66
+ color="#333333",
67
+ va="top",
68
+ )
69
+ # 默认单排
70
+ self.bar_groups = [
71
+ self.ax.bar(range(self.n), self.data, color=self.NORMAL)
72
+ ]
73
+ self.multi = False
74
+ self.ani = None
75
+
76
+ def _resolve_arrays(self, arrays):
77
+ """判断并统一成 list-of-lists;返回 (arrays_list, multi_flag)"""
78
+ if isinstance(arrays, list) and arrays and isinstance(arrays[0], list):
79
+ return arrays, True
80
+ return [arrays], False
81
+
82
+ def _ensure_bars(self, arrays_list, multi):
83
+ """根据数组数量/长度重建柱子;arr 在 idx=0 → bottom=0 → 最底下"""
84
+ target = len(arrays_list)
85
+ need_rebuild = self.multi != multi or len(self.bar_groups) != target
86
+ if not need_rebuild:
87
+ # 更新高度即可
88
+ for group, arr in zip(self.bar_groups, arrays_list):
89
+ for idx, b in enumerate(group):
90
+ if idx < len(arr):
91
+ b.set_height(arr[idx])
92
+ return
93
+
94
+ # 重建
95
+ for group in self.bar_groups:
96
+ for b in group:
97
+ b.remove()
98
+ self.bar_groups = []
99
+ self.multi = multi
100
+
101
+ if multi:
102
+ max_val = max(max(a) for a in arrays_list)
103
+ gap = max_val * 0.2
104
+ for idx, arr in enumerate(arrays_list):
105
+ bottom = idx * (max_val + gap)
106
+ self.bar_groups.append(
107
+ self.ax.bar(
108
+ range(len(arr)), arr, bottom=bottom, color=self.NORMAL, width=0.8
109
+ )
110
+ )
111
+ top = (len(arrays_list) - 1) * (max_val + gap) + max_val + gap
112
+ self.ax.set_ylim(0, top)
113
+ else:
114
+ self.bar_groups = [
115
+ self.ax.bar(range(self.n), arrays_list[0], color=self.NORMAL)
116
+ ]
117
+
118
+ @staticmethod
119
+ def _normalize_hl(d, n):
120
+ """把单排的 hl 规格转成 {idx: color} 字典"""
121
+ if isinstance(d, dict):
122
+ return {k: v for k, v in d.items() if 0 <= k < n}
123
+ return {k: Silly.HIGHLIGHT for k in d if 0 <= k < n}
124
+
125
+ def _update(self, frame):
126
+ arrays, hl, text = frame
127
+
128
+ arrays_list, multi = self._resolve_arrays(arrays)
129
+ self._ensure_bars(arrays_list, multi)
130
+
131
+ if multi:
132
+ if isinstance(hl, list):
133
+ hl_list = list(hl) + [{}] * (len(arrays_list) - len(hl))
134
+ else:
135
+ hl_list = [hl] * len(arrays_list)
136
+ else:
137
+ hl_list = [hl]
138
+
139
+ artists = []
140
+ for g_idx, (group, arr) in enumerate(zip(self.bar_groups, arrays_list)):
141
+ clean = self._normalize_hl(hl_list[g_idx], len(arr))
142
+ for idx, b in enumerate(group):
143
+ b.set_height(arr[idx])
144
+ b.set_color(clean.get(idx, self.NORMAL))
145
+ artists.append(b)
146
+
147
+ self.info_text.set_text(text)
148
+ artists.append(self.info_text)
149
+ return artists
150
+
151
+ def go(self, sort_gen):
152
+ self.ani = FuncAnimation(
153
+ self.fig,
154
+ self._update,
155
+ frames=sort_gen(self.data),
156
+ interval=0,
157
+ repeat=False,
158
+ blit=True,
159
+ cache_frame_data=False,
160
+ )
161
+ plt.tight_layout()
162
+ plt.show()
163
+ return self
164
+
165
+
166
+ def frame(*pairs, text=""):
167
+ """按名字组装多排帧。pairs 为若干 (array, highlight) 元组。"""
168
+ arrays = [a.copy() for a, _ in pairs]
169
+ hls = [h for _, h in pairs]
170
+ return (arrays, hls, text)