evolver-tools 1.4.0__py3-none-any.whl
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.
- evolver_tools/__init__.py +2 -0
- evolver_tools/__main__.py +3 -0
- evolver_tools/cli.py +89 -0
- evolver_tools/vendor/b64/__init__.py +2 -0
- evolver_tools/vendor/b64/b64.py +176 -0
- evolver_tools/vendor/cal_tool/__init__.py +1 -0
- evolver_tools/vendor/cal_tool/cli.py +234 -0
- evolver_tools/vendor/chart_cli/__init__.py +444 -0
- evolver_tools/vendor/chart_cli/__main__.py +3 -0
- evolver_tools/vendor/colors/__init__.py +5 -0
- evolver_tools/vendor/colors/__main__.py +97 -0
- evolver_tools/vendor/csv_stats/__init__.py +5 -0
- evolver_tools/vendor/csv_stats/__main__.py +4 -0
- evolver_tools/vendor/csv_stats/analyzer.py +258 -0
- evolver_tools/vendor/csv_stats/cli.py +45 -0
- evolver_tools/vendor/dirsize/__init__.py +183 -0
- evolver_tools/vendor/envcheck/__init__.py +426 -0
- evolver_tools/vendor/ff/__init__.py +427 -0
- evolver_tools/vendor/ff/__main__.py +3 -0
- evolver_tools/vendor/find_dups/__init__.py +7 -0
- evolver_tools/vendor/find_dups/cli.py +392 -0
- evolver_tools/vendor/hashsum/__init__.py +211 -0
- evolver_tools/vendor/hashsum/__main__.py +5 -0
- evolver_tools/vendor/http_live/__init__.py +265 -0
- evolver_tools/vendor/http_live/__main__.py +2 -0
- evolver_tools/vendor/ipinfo/__init__.py +3 -0
- evolver_tools/vendor/ipinfo/__main__.py +30 -0
- evolver_tools/vendor/jq_lite/__init__.py +257 -0
- evolver_tools/vendor/jq_lite/__main__.py +5 -0
- evolver_tools/vendor/json2csv/__init__.py +3 -0
- evolver_tools/vendor/json2csv/__main__.py +82 -0
- evolver_tools/vendor/jsonql/__init__.py +326 -0
- evolver_tools/vendor/jsonql/__main__.py +5 -0
- evolver_tools/vendor/license_cli/__init__.py +1 -0
- evolver_tools/vendor/license_cli/__main__.py +4 -0
- evolver_tools/vendor/license_cli/cli.py +289 -0
- evolver_tools/vendor/markdown_check/__init__.py +211 -0
- evolver_tools/vendor/nb/__init__.py +319 -0
- evolver_tools/vendor/nb/__main__.py +3 -0
- evolver_tools/vendor/passgen/__init__.py +224 -0
- evolver_tools/vendor/portcheck/__init__.py +2 -0
- evolver_tools/vendor/portcheck/__main__.py +66 -0
- evolver_tools/vendor/project_doctor/__init__.py +412 -0
- evolver_tools/vendor/project_doctor/__main__.py +3 -0
- evolver_tools/vendor/ren/__init__.py +283 -0
- evolver_tools/vendor/ren/__main__.py +3 -0
- evolver_tools/vendor/siege_lite/__init__.py +250 -0
- evolver_tools/vendor/siege_lite/__main__.py +3 -0
- evolver_tools/vendor/smellfinder/__init__.py +376 -0
- evolver_tools/vendor/smellfinder/__main__.py +3 -0
- evolver_tools/vendor/sqlite_cli/__init__.py +326 -0
- evolver_tools/vendor/sqlite_cli/__main__.py +5 -0
- evolver_tools/vendor/sysmon/__init__.py +299 -0
- evolver_tools/vendor/sysmon/__main__.py +3 -0
- evolver_tools/vendor/timer/__init__.py +127 -0
- evolver_tools/vendor/treedir/__init__.py +2 -0
- evolver_tools/vendor/treedir/__main__.py +128 -0
- evolver_tools/vendor/urlparse_tool/__init__.py +3 -0
- evolver_tools/vendor/urlparse_tool/cli.py +212 -0
- evolver_tools/vendor/web_summary/__init__.py +341 -0
- evolver_tools/vendor/web_summary/__main__.py +3 -0
- evolver_tools/vendor/wordcount/__init__.py +2 -0
- evolver_tools/vendor/wordcount/__main__.py +101 -0
- evolver_tools-1.4.0.dist-info/METADATA +107 -0
- evolver_tools-1.4.0.dist-info/RECORD +69 -0
- evolver_tools-1.4.0.dist-info/WHEEL +5 -0
- evolver_tools-1.4.0.dist-info/entry_points.txt +34 -0
- evolver_tools-1.4.0.dist-info/licenses/LICENSE +21 -0
- evolver_tools-1.4.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
chart-cli — 终端图表生成器
|
|
4
|
+
用 Unicode 在终端绘制条形图、折线图、饼图。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import sys
|
|
8
|
+
import json
|
|
9
|
+
import math
|
|
10
|
+
from collections import Counter
|
|
11
|
+
|
|
12
|
+
# Unicode block characters
|
|
13
|
+
BAR_FULL = '█'
|
|
14
|
+
BAR_HALF = '▌'
|
|
15
|
+
BAR_EMPTY = '░'
|
|
16
|
+
H_LINE = '─'
|
|
17
|
+
V_LINE = '│'
|
|
18
|
+
CORNER_TL = '┌'
|
|
19
|
+
CORNER_TR = '┐'
|
|
20
|
+
CORNER_BL = '└'
|
|
21
|
+
CORNER_BR = '┘'
|
|
22
|
+
CROSS = '┼'
|
|
23
|
+
TEE_DOWN = '┬'
|
|
24
|
+
TEE_UP = '┴'
|
|
25
|
+
TEE_RIGHT = '├'
|
|
26
|
+
TEE_LEFT = '┤'
|
|
27
|
+
|
|
28
|
+
# 8-level block for fine-grained bars
|
|
29
|
+
BLOCKS = [' ', '▏', '▎', '▍', '▌', '▋', '▊', '▉', '█']
|
|
30
|
+
|
|
31
|
+
def red(s): return f"\033[91m{s}\033[0m"
|
|
32
|
+
def green(s): return f"\033[92m{s}\033[0m"
|
|
33
|
+
def yellow(s): return f"\033[93m{s}\033[0m"
|
|
34
|
+
def cyan(s): return f"\033[96m{s}\033[0m"
|
|
35
|
+
def dim(s): return f"\033[2m{s}\033[0m"
|
|
36
|
+
def bold(s): return f"\033[1m{s}\033[0m"
|
|
37
|
+
|
|
38
|
+
def parse_data(source):
|
|
39
|
+
"""Parse data from string or dict"""
|
|
40
|
+
if isinstance(source, list):
|
|
41
|
+
return source
|
|
42
|
+
if isinstance(source, dict):
|
|
43
|
+
return list(source.items())
|
|
44
|
+
# Try parsing as JSON
|
|
45
|
+
try:
|
|
46
|
+
parsed = json.loads(source)
|
|
47
|
+
if isinstance(parsed, list):
|
|
48
|
+
# Handle flat alternating array: ["key1", val1, "key2", val2, ...]
|
|
49
|
+
if parsed and not isinstance(parsed[0], (list, tuple)):
|
|
50
|
+
if len(parsed) % 2 == 0:
|
|
51
|
+
return [(str(parsed[i]), float(parsed[i+1])) for i in range(0, len(parsed), 2)]
|
|
52
|
+
else:
|
|
53
|
+
raise ValueError("数组格式应为 [键1, 值1, 键2, 值2, ...] (偶数个元素)")
|
|
54
|
+
return parsed
|
|
55
|
+
if isinstance(parsed, dict):
|
|
56
|
+
return list(parsed.items())
|
|
57
|
+
except json.JSONDecodeError:
|
|
58
|
+
pass
|
|
59
|
+
# Try parsing as key:value lines
|
|
60
|
+
result = []
|
|
61
|
+
# Split by commas first (for inline lists)
|
|
62
|
+
lines = []
|
|
63
|
+
for line in source.strip().split('\n'):
|
|
64
|
+
if ',' in line and not line.startswith('{') and not line.startswith('['):
|
|
65
|
+
# Could be comma-separated pairs
|
|
66
|
+
parts = line.split(',')
|
|
67
|
+
for p in parts:
|
|
68
|
+
p = p.strip()
|
|
69
|
+
if p:
|
|
70
|
+
lines.append(p)
|
|
71
|
+
else:
|
|
72
|
+
if line.strip():
|
|
73
|
+
lines.append(line.strip())
|
|
74
|
+
|
|
75
|
+
for raw_line in lines:
|
|
76
|
+
if ':' in raw_line:
|
|
77
|
+
parts = raw_line.split(':', 1)
|
|
78
|
+
key = parts[0].strip()
|
|
79
|
+
try:
|
|
80
|
+
val = float(parts[1].strip())
|
|
81
|
+
result.append((key, val))
|
|
82
|
+
except ValueError:
|
|
83
|
+
pass
|
|
84
|
+
elif '\t' in raw_line:
|
|
85
|
+
parts = raw_line.split('\t')
|
|
86
|
+
try:
|
|
87
|
+
val = float(parts[-1].strip())
|
|
88
|
+
key = ' '.join(parts[:-1]).strip()
|
|
89
|
+
result.append((key, val))
|
|
90
|
+
except ValueError:
|
|
91
|
+
pass
|
|
92
|
+
else:
|
|
93
|
+
# Try as raw number
|
|
94
|
+
try:
|
|
95
|
+
val = float(raw_line.strip())
|
|
96
|
+
result.append((raw_line.strip(), val))
|
|
97
|
+
except ValueError:
|
|
98
|
+
pass
|
|
99
|
+
return result
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def draw_bar_chart(data, width=40, height=12, horizontal=False, sort=False, show_values=True):
|
|
103
|
+
"""Draw a bar chart"""
|
|
104
|
+
if not data:
|
|
105
|
+
return "(无数据)"
|
|
106
|
+
|
|
107
|
+
if sort:
|
|
108
|
+
data = sorted(data, key=lambda x: x[1], reverse=True)
|
|
109
|
+
|
|
110
|
+
labels = [str(d[0])[:20] for d in data]
|
|
111
|
+
values = [float(d[1]) for d in data]
|
|
112
|
+
|
|
113
|
+
max_val = max(values) if values else 1
|
|
114
|
+
min_val = min(values)
|
|
115
|
+
lines = []
|
|
116
|
+
|
|
117
|
+
if horizontal:
|
|
118
|
+
# Horizontal bar chart
|
|
119
|
+
max_label_len = max(len(l) for l in labels)
|
|
120
|
+
max_label_len = min(max_label_len, 20)
|
|
121
|
+
|
|
122
|
+
lines.append(f" {bold('条形图 (水平)')}")
|
|
123
|
+
lines.append("")
|
|
124
|
+
|
|
125
|
+
for i, (label, val) in enumerate(zip(labels, values)):
|
|
126
|
+
bar_width = int((val / max_val) * width) if max_val > 0 else 0
|
|
127
|
+
bar = BAR_FULL * bar_width
|
|
128
|
+
val_str = f" {val}" if show_values else ""
|
|
129
|
+
padded_label = label.ljust(max_label_len)[:max_label_len]
|
|
130
|
+
lines.append(f" {dim(padded_label)} {dim('│')} {green(bar)}{val_str}")
|
|
131
|
+
|
|
132
|
+
lines.append(f" {dim('─' * (max_label_len + 3 + width))}")
|
|
133
|
+
lines.append(f" {dim(('0').rjust(max_label_len + 3))} {max_val})")
|
|
134
|
+
|
|
135
|
+
else:
|
|
136
|
+
# Vertical bar chart
|
|
137
|
+
max_label_len = max(len(l) for l in labels)
|
|
138
|
+
n_bars = len(labels)
|
|
139
|
+
|
|
140
|
+
# Calculate available height for bars (leave room for labels + values)
|
|
141
|
+
bar_area_height = max(5, height - 3)
|
|
142
|
+
|
|
143
|
+
lines.append(f" {bold('条形图 (垂直)')}")
|
|
144
|
+
lines.append("")
|
|
145
|
+
|
|
146
|
+
# Draw bars top to bottom
|
|
147
|
+
for row in range(bar_area_height, 0, -1):
|
|
148
|
+
threshold = max_val * (row - 1) / bar_area_height if bar_area_height > 0 else 0
|
|
149
|
+
|
|
150
|
+
line_parts = [" "]
|
|
151
|
+
for val in values:
|
|
152
|
+
if val >= threshold:
|
|
153
|
+
if val >= max_val * row / bar_area_height:
|
|
154
|
+
line_parts.append(f" {BAR_FULL} ")
|
|
155
|
+
else:
|
|
156
|
+
line_parts.append(f" {BAR_HALF} ")
|
|
157
|
+
else:
|
|
158
|
+
line_parts.append(f" ")
|
|
159
|
+
lines.append(''.join(line_parts))
|
|
160
|
+
|
|
161
|
+
# Axis line
|
|
162
|
+
lines.append(f" {'─' * (n_bars * 3 + 1)}")
|
|
163
|
+
|
|
164
|
+
# Labels
|
|
165
|
+
line_parts = [" "]
|
|
166
|
+
for label in labels:
|
|
167
|
+
short = label[:3]
|
|
168
|
+
line_parts.append(f" {short} ")
|
|
169
|
+
lines.append(''.join(line_parts))
|
|
170
|
+
|
|
171
|
+
# Value labels
|
|
172
|
+
if show_values:
|
|
173
|
+
line_parts = [" "]
|
|
174
|
+
for val in values:
|
|
175
|
+
s = str(int(val)) if val == int(val) else f"{val:.1f}"
|
|
176
|
+
line_parts.append(f"{s:>3} ")
|
|
177
|
+
lines.append(''.join(line_parts))
|
|
178
|
+
|
|
179
|
+
return '\n'.join(lines)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def draw_line_chart(data, width=50, height=14):
|
|
183
|
+
"""Draw a line chart"""
|
|
184
|
+
if not data:
|
|
185
|
+
return "(无数据)"
|
|
186
|
+
|
|
187
|
+
# If data is list of (x, y) tuples
|
|
188
|
+
if isinstance(data[0], (list, tuple)) and len(data[0]) >= 2:
|
|
189
|
+
points = [(float(d[0]), float(d[1])) for d in data]
|
|
190
|
+
else:
|
|
191
|
+
# Assume index as x
|
|
192
|
+
points = [(float(i), float(d[1]) if isinstance(d, (list, tuple)) else float(d))
|
|
193
|
+
for i, d in enumerate(data)]
|
|
194
|
+
|
|
195
|
+
if len(points) < 2:
|
|
196
|
+
return "(至少需要2个数据点)"
|
|
197
|
+
|
|
198
|
+
xs = [p[0] for p in points]
|
|
199
|
+
ys = [p[1] for p in points]
|
|
200
|
+
min_x, max_x = min(xs), max(xs)
|
|
201
|
+
min_y, max_y = min(ys), max(ys)
|
|
202
|
+
range_y = max_y - min_y if max_y != min_y else 1
|
|
203
|
+
range_x = max_x - min_x if max_x != min_x else 1
|
|
204
|
+
|
|
205
|
+
chart_width = max(20, width - 6) # Leave room for y-axis labels
|
|
206
|
+
chart_height = max(5, height - 3)
|
|
207
|
+
|
|
208
|
+
lines = []
|
|
209
|
+
lines.append(f" {bold('折线图')}")
|
|
210
|
+
lines.append("")
|
|
211
|
+
|
|
212
|
+
# Create the chart grid
|
|
213
|
+
grid = [[' ' for _ in range(chart_width)] for _ in range(chart_height)]
|
|
214
|
+
|
|
215
|
+
# Plot points
|
|
216
|
+
for x, y in points:
|
|
217
|
+
col = int((x - min_x) / range_x * (chart_width - 1))
|
|
218
|
+
row = int((max_y - y) / range_y * (chart_height - 1))
|
|
219
|
+
col = min(max(0, col), chart_width - 1)
|
|
220
|
+
row = min(max(0, row), chart_height - 1)
|
|
221
|
+
grid[row][col] = '•'
|
|
222
|
+
|
|
223
|
+
# Draw lines between points
|
|
224
|
+
for i in range(len(points) - 1):
|
|
225
|
+
x1, y1 = points[i]
|
|
226
|
+
x2, y2 = points[i+1]
|
|
227
|
+
c1 = int((x1 - min_x) / range_x * (chart_width - 1))
|
|
228
|
+
c2 = int((x2 - min_x) / range_x * (chart_width - 1))
|
|
229
|
+
r1 = int((max_y - y1) / range_y * (chart_height - 1))
|
|
230
|
+
r2 = int((max_y - y2) / range_y * (chart_height - 1))
|
|
231
|
+
|
|
232
|
+
# Bresenham-like line drawing
|
|
233
|
+
steps = max(abs(c2 - c1), abs(r2 - r1))
|
|
234
|
+
if steps > 1:
|
|
235
|
+
for t in range(1, steps):
|
|
236
|
+
frac = t / steps
|
|
237
|
+
cx = int(c1 + (c2 - c1) * frac)
|
|
238
|
+
cy = int(r1 + (r2 - r1) * frac)
|
|
239
|
+
cx = min(max(0, cx), chart_width - 1)
|
|
240
|
+
cy = min(max(0, cy), chart_height - 1)
|
|
241
|
+
if grid[cy][cx] == ' ':
|
|
242
|
+
grid[cy][cx] = '·'
|
|
243
|
+
|
|
244
|
+
# Y-axis labels
|
|
245
|
+
y_labels = []
|
|
246
|
+
for i in range(chart_height):
|
|
247
|
+
val = max_y - (range_y * i / (chart_height - 1))
|
|
248
|
+
y_labels.append(f"{val:>6.1f}")
|
|
249
|
+
|
|
250
|
+
# Print chart
|
|
251
|
+
for i in range(chart_height):
|
|
252
|
+
label = y_labels[i]
|
|
253
|
+
row_line = ''.join(grid[i])
|
|
254
|
+
lines.append(f" {dim(label)} {dim(V_LINE)} {cyan(row_line)}")
|
|
255
|
+
|
|
256
|
+
# X-axis
|
|
257
|
+
x_label_fmt = f"{min_x:.1f}".rjust(chart_width // 2) + f"{max_x:.1f}".rjust(chart_width // 2)
|
|
258
|
+
lines.append(f" {dim(' ')} {dim('└')}{dim('─' * chart_width)}")
|
|
259
|
+
lines.append(f" {dim(' ')} {x_label_fmt}")
|
|
260
|
+
|
|
261
|
+
return '\n'.join(lines)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def draw_pie_chart(data, radius=8):
|
|
265
|
+
"""Draw a pie chart using braille/block characters"""
|
|
266
|
+
if not data:
|
|
267
|
+
return "(无数据)"
|
|
268
|
+
|
|
269
|
+
total = sum(float(d[1]) for d in data)
|
|
270
|
+
if total == 0:
|
|
271
|
+
return "(总和为零)"
|
|
272
|
+
|
|
273
|
+
labels = [str(d[0])[:15] for d in data]
|
|
274
|
+
values = [float(d[1]) for d in data]
|
|
275
|
+
|
|
276
|
+
# Calculate angles (in radians, starting from top)
|
|
277
|
+
angles = []
|
|
278
|
+
cumulative = 0
|
|
279
|
+
for val in values:
|
|
280
|
+
angle = (val / total) * 2 * math.pi
|
|
281
|
+
cumulative += angle
|
|
282
|
+
angles.append(cumulative)
|
|
283
|
+
|
|
284
|
+
# Round to first angle = 0
|
|
285
|
+
angles = [0] + angles
|
|
286
|
+
|
|
287
|
+
# Available colors
|
|
288
|
+
colors = [
|
|
289
|
+
'\033[91m', '\033[93m', '\033[92m', '\033[96m',
|
|
290
|
+
'\033[94m', '\033[95m', '\033[38;5;208m', '\033[38;5;45m',
|
|
291
|
+
]
|
|
292
|
+
reset = '\033[0m'
|
|
293
|
+
|
|
294
|
+
diameter = radius * 2 + 1
|
|
295
|
+
center_x = radius
|
|
296
|
+
center_y = radius
|
|
297
|
+
|
|
298
|
+
lines = []
|
|
299
|
+
lines.append(f" {bold('饼图')}")
|
|
300
|
+
lines.append("")
|
|
301
|
+
|
|
302
|
+
# Generate pie
|
|
303
|
+
pie_chars = []
|
|
304
|
+
for y in range(diameter):
|
|
305
|
+
row_chars = []
|
|
306
|
+
for x in range(diameter):
|
|
307
|
+
# Distance from center
|
|
308
|
+
dx = x - center_x
|
|
309
|
+
dy = y - center_y
|
|
310
|
+
dist = math.sqrt(dx * dx + dy * dy)
|
|
311
|
+
|
|
312
|
+
if dist < radius - 0.5:
|
|
313
|
+
# Inside: check angle
|
|
314
|
+
angle = math.atan2(dx, -dy) # Atan2, with top as 0
|
|
315
|
+
if angle < 0:
|
|
316
|
+
angle += 2 * math.pi
|
|
317
|
+
|
|
318
|
+
# Find which segment
|
|
319
|
+
seg_idx = -1
|
|
320
|
+
for i in range(len(angles) - 1):
|
|
321
|
+
if angles[i] <= angle < angles[i + 1]:
|
|
322
|
+
seg_idx = i
|
|
323
|
+
break
|
|
324
|
+
if seg_idx == -1 and angle >= angles[-2]:
|
|
325
|
+
seg_idx = len(angles) - 2
|
|
326
|
+
|
|
327
|
+
if 0 <= seg_idx < len(colors):
|
|
328
|
+
# Draw filled with dithering
|
|
329
|
+
# Use different chars for different segments
|
|
330
|
+
chars = ['█', '▓', '▒', '░']
|
|
331
|
+
c = colors[seg_idx % len(colors)]
|
|
332
|
+
row_chars.append(f"{c}{chr(0x2588)}{reset}")
|
|
333
|
+
else:
|
|
334
|
+
row_chars.append(' ')
|
|
335
|
+
elif dist < radius + 0.5:
|
|
336
|
+
row_chars.append(f"{dim('·')} ")
|
|
337
|
+
else:
|
|
338
|
+
row_chars.append(' ')
|
|
339
|
+
pie_chars.append(''.join(row_chars))
|
|
340
|
+
|
|
341
|
+
# Print pie + legend side by side
|
|
342
|
+
legend_lines = []
|
|
343
|
+
for i in range(len(data)):
|
|
344
|
+
c = colors[i % len(colors)]
|
|
345
|
+
pct = values[i] / total * 100
|
|
346
|
+
legend_lines.append(f" {c}█{reset} {labels[i]:<15} {values[i]:>8} ({pct:4.1f}%)")
|
|
347
|
+
|
|
348
|
+
max_lines = max(len(pie_chars), len(legend_lines))
|
|
349
|
+
for i in range(max_lines):
|
|
350
|
+
left = pie_chars[i] if i < len(pie_chars) else ' ' * (diameter * 2)
|
|
351
|
+
right = legend_lines[i] if i < len(legend_lines) else ''
|
|
352
|
+
print(f" {left} {right}")
|
|
353
|
+
|
|
354
|
+
return "\n" # Already printed during drawing
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def draw_histogram_chart(data, bins=10, width=40):
|
|
358
|
+
"""Draw a histogram from raw values"""
|
|
359
|
+
if not data:
|
|
360
|
+
return "(无数据)"
|
|
361
|
+
|
|
362
|
+
values = [float(d) if isinstance(d, (int, float)) else float(d[1]) for d in data]
|
|
363
|
+
if not values:
|
|
364
|
+
return "(无数据)"
|
|
365
|
+
|
|
366
|
+
min_v = min(values)
|
|
367
|
+
max_v = max(values)
|
|
368
|
+
if min_v == max_v:
|
|
369
|
+
return f" 所有值相同: {min_v}"
|
|
370
|
+
|
|
371
|
+
range_v = max_v - min_v
|
|
372
|
+
bucket_size = range_v / bins
|
|
373
|
+
buckets = [0] * bins
|
|
374
|
+
|
|
375
|
+
for v in values:
|
|
376
|
+
idx = min(int((v - min_v) / bucket_size), bins - 1)
|
|
377
|
+
buckets[idx] += 1
|
|
378
|
+
|
|
379
|
+
max_count = max(buckets)
|
|
380
|
+
lines = []
|
|
381
|
+
lines.append(f" {bold('直方图')}")
|
|
382
|
+
lines.append(f" {dim(f'区间数: {bins}, 总值: {len(values)}')}")
|
|
383
|
+
lines.append("")
|
|
384
|
+
|
|
385
|
+
for i in range(bins):
|
|
386
|
+
lo = min_v + i * bucket_size
|
|
387
|
+
hi = lo + bucket_size
|
|
388
|
+
count = buckets[i]
|
|
389
|
+
bar_len = int(count / max_count * width) if max_count > 0 else 0
|
|
390
|
+
bar = BAR_FULL * bar_len
|
|
391
|
+
|
|
392
|
+
if i == 0:
|
|
393
|
+
label = f"{lo:.1f}-{hi:.1f}"
|
|
394
|
+
elif i == bins - 1:
|
|
395
|
+
label = f"{lo:.1f}+"
|
|
396
|
+
else:
|
|
397
|
+
label = f"{lo:.1f}"
|
|
398
|
+
|
|
399
|
+
pct = count / len(values) * 100
|
|
400
|
+
lines.append(f" {dim(label):<16} {green(bar)} {count} ({pct:.1f}%)")
|
|
401
|
+
|
|
402
|
+
return '\n'.join(lines)
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def main():
|
|
406
|
+
import argparse
|
|
407
|
+
parser = argparse.ArgumentParser(description='终端图表生成器')
|
|
408
|
+
parser.add_argument('type', choices=['bar', 'line', 'pie', 'hist'],
|
|
409
|
+
help='图表类型')
|
|
410
|
+
parser.add_argument('data', nargs='?', help='数据(JSON 或 key:value 格式,或从 stdin 读取)')
|
|
411
|
+
parser.add_argument('-w', '--width', type=int, default=40, help='图表宽度')
|
|
412
|
+
parser.add_argument('-H', '--height', type=int, default=12, help='图表高度')
|
|
413
|
+
parser.add_argument('--horizontal', action='store_true', help='条形图水平模式')
|
|
414
|
+
parser.add_argument('--sort', action='store_true', help='排序(条形图)')
|
|
415
|
+
parser.add_argument('--no-values', action='store_true', help='隐藏数值')
|
|
416
|
+
args = parser.parse_args()
|
|
417
|
+
|
|
418
|
+
# Read data
|
|
419
|
+
data_source = args.data
|
|
420
|
+
if not data_source and not sys.stdin.isatty():
|
|
421
|
+
data_source = sys.stdin.read()
|
|
422
|
+
|
|
423
|
+
if not data_source:
|
|
424
|
+
print("错误: 请提供数据(参数或 stdin)")
|
|
425
|
+
sys.exit(1)
|
|
426
|
+
|
|
427
|
+
data = parse_data(data_source)
|
|
428
|
+
if not data:
|
|
429
|
+
print("错误: 无法解析数据。支持 JSON 或 key:value 格式")
|
|
430
|
+
sys.exit(1)
|
|
431
|
+
|
|
432
|
+
if args.type == 'bar':
|
|
433
|
+
print(draw_bar_chart(data, args.width, args.height,
|
|
434
|
+
args.horizontal, args.sort, not args.no_values))
|
|
435
|
+
elif args.type == 'line':
|
|
436
|
+
print(draw_line_chart(data, args.width, args.height))
|
|
437
|
+
elif args.type == 'pie':
|
|
438
|
+
draw_pie_chart(data, args.radius if hasattr(args, 'radius') else 8)
|
|
439
|
+
elif args.type == 'hist':
|
|
440
|
+
print(draw_histogram_chart(data, 10, args.width))
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
if __name__ == '__main__':
|
|
444
|
+
main()
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
|
|
2
|
+
import sys
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
def hex_to_rgb(h):
|
|
6
|
+
h = h.lstrip('#')
|
|
7
|
+
if len(h) == 3:
|
|
8
|
+
h = ''.join(c*2 for c in h)
|
|
9
|
+
return tuple(int(h[i:i+2], 16) for i in (0, 2, 4))
|
|
10
|
+
|
|
11
|
+
def rgb_to_hex(r, g, b):
|
|
12
|
+
return f'#{r:02x}{g:02x}{b:02x}'
|
|
13
|
+
|
|
14
|
+
def hsl_to_rgb(h, s, l):
|
|
15
|
+
h = h / 360.0
|
|
16
|
+
s = s / 100.0
|
|
17
|
+
l = l / 100.0
|
|
18
|
+
def hue2rgb(p, q, t):
|
|
19
|
+
if t < 0: t += 1
|
|
20
|
+
if t > 1: t -= 1
|
|
21
|
+
if t < 1/6: return p + (q - p) * 6 * t
|
|
22
|
+
if t < 1/2: return q
|
|
23
|
+
if t < 2/3: return p + (q - p) * (2/3 - t) * 6
|
|
24
|
+
return p
|
|
25
|
+
if s == 0:
|
|
26
|
+
r = g = b = l
|
|
27
|
+
else:
|
|
28
|
+
q = l * (1 + s) if l < 0.5 else l + s - l * s
|
|
29
|
+
p = 2 * l - q
|
|
30
|
+
r = hue2rgb(p, q, h + 1/3)
|
|
31
|
+
g = hue2rgb(p, q, h)
|
|
32
|
+
b = hue2rgb(p, q, h - 1/3)
|
|
33
|
+
return (round(r*255), round(g*255), round(b*255))
|
|
34
|
+
|
|
35
|
+
def show_256():
|
|
36
|
+
print("\n 256-color chart (bg):")
|
|
37
|
+
for i in range(0, 16, 8):
|
|
38
|
+
row = ''.join(f'\x1b[48;5;{i+j}m {i+j:>3} \x1b[0m' for j in range(8))
|
|
39
|
+
print(f' {row}')
|
|
40
|
+
for block in range(16, 232, 36):
|
|
41
|
+
print()
|
|
42
|
+
for i in range(block, block+36, 6):
|
|
43
|
+
row = ''.join(f'\x1b[48;5;{i+j}m {i+j:>3} \x1b[0m' for j in range(6) if i+j < 232)
|
|
44
|
+
print(f' {row}')
|
|
45
|
+
print()
|
|
46
|
+
print(" Grayscale (232-255):")
|
|
47
|
+
row = ''.join(f'\x1b[48;5;{i}m {i:>3} \x1b[0m' for i in range(232, 256))
|
|
48
|
+
print(f' {row}')
|
|
49
|
+
print('\x1b[0m')
|
|
50
|
+
|
|
51
|
+
def show_basic():
|
|
52
|
+
print("\n Basic 16 colors:")
|
|
53
|
+
for i in range(8):
|
|
54
|
+
fg = f'\x1b[38;5;{i}m'
|
|
55
|
+
bg = f'\x1b[48;5;{i}m'
|
|
56
|
+
bright = f'\x1b[38;5;{i+8}m'
|
|
57
|
+
print(f' {fg}Color {i}\x1b[0m {bright}Color {i+8}\x1b[0m {bg} \x1b[0m')
|
|
58
|
+
|
|
59
|
+
def main():
|
|
60
|
+
args = sys.argv[1:]
|
|
61
|
+
if not args:
|
|
62
|
+
show_basic()
|
|
63
|
+
show_256()
|
|
64
|
+
return
|
|
65
|
+
|
|
66
|
+
cmd = args[0]
|
|
67
|
+
|
|
68
|
+
if cmd == "256":
|
|
69
|
+
show_256()
|
|
70
|
+
elif cmd == "basic":
|
|
71
|
+
show_basic()
|
|
72
|
+
elif cmd in ("hex", "h") and len(args) >= 2:
|
|
73
|
+
try:
|
|
74
|
+
r, g, b = hex_to_rgb(args[1])
|
|
75
|
+
h, s, v = 0, 0, 0
|
|
76
|
+
print(f"HEX: {args[1].lower()}")
|
|
77
|
+
print(f"RGB: {r}, {g}, {b}")
|
|
78
|
+
print(f"\x1b[48;2;{r};{g};{b}m Sample \x1b[0m")
|
|
79
|
+
except:
|
|
80
|
+
print("Usage: colors hex #ff6600")
|
|
81
|
+
elif cmd in ("rgb", "r") and len(args) >= 4:
|
|
82
|
+
try:
|
|
83
|
+
r, g, b = int(args[1]), int(args[2]), int(args[3])
|
|
84
|
+
print(f"RGB: {r}, {g}, {b}")
|
|
85
|
+
print(f"HEX: {rgb_to_hex(r, g, b)}")
|
|
86
|
+
print(f"\x1b[48;2;{r};{g};{b}m Sample \x1b[0m")
|
|
87
|
+
except:
|
|
88
|
+
print("Usage: colors rgb 255 102 0")
|
|
89
|
+
else:
|
|
90
|
+
print("Usage: colors [hex|rgb|256|basic] [args...]")
|
|
91
|
+
print(" colors — show all color charts")
|
|
92
|
+
print(" colors 256 — 256-color chart")
|
|
93
|
+
print(" colors hex #ff0 — convert hex to RGB with preview")
|
|
94
|
+
print(" colors rgb 255 102 0 — convert RGB to hex with preview")
|
|
95
|
+
|
|
96
|
+
if __name__ == "__main__":
|
|
97
|
+
main()
|