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.
- sillyvis-0.1.1/.gitignore +21 -0
- sillyvis-0.1.1/PKG-INFO +7 -0
- sillyvis-0.1.1/README.md +59 -0
- sillyvis-0.1.1/examples/bubble_sort.py +29 -0
- sillyvis-0.1.1/examples/merge_sort.py +58 -0
- sillyvis-0.1.1/pyproject.toml +15 -0
- sillyvis-0.1.1/src/sillyvis/__init__.py +6 -0
- sillyvis-0.1.1/src/sillyvis/core.py +170 -0
sillyvis-0.1.1/PKG-INFO
ADDED
sillyvis-0.1.1/README.md
ADDED
|
@@ -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,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)
|