math-tutor/tutor/visualize.py

141 lines
6.3 KiB
Python

"""
数学可视化引擎 — 生成 SVG 图形
学生喜欢画画 → 用 SVG 让他们「看到」数学
"""
import math
def pie_chart(fractions: list, title: str = "", size: int = 200) -> str:
"""分数饼图 — 用于理解分数概念"""
colors = ["#FF6B6B", "#4ECDC4", "#45B7D1", "#96CEB4", "#FFEAA7",
"#DDA0DD", "#98D8C8", "#F7DC6F", "#BB8FCE", "#85C1E9"]
total = sum(fractions)
if total == 0:
return ""
cx, cy, r = size // 2, size // 2, size // 2 - 10
paths = []
start_angle = -90 # 从顶部开始
for i, val in enumerate(fractions):
angle = (val / total) * 360
end_angle = start_angle + angle
# 计算弧线坐标
sr = math.radians(start_angle)
er = math.radians(end_angle)
x1 = cx + r * math.cos(sr)
y1 = cy + r * math.sin(sr)
x2 = cx + r * math.cos(er)
y2 = cy + r * math.sin(er)
large_arc = 1 if angle > 180 else 0
path = f'<path d="M{cx},{cy} L{x1:.1f},{y1:.1f} A{r},{r} 0 {large_arc},1 {x2:.1f},{y2:.1f} Z" fill="{colors[i % len(colors)]}" stroke="white" stroke-width="2"/>'
paths.append(path)
# 标签
mid = math.radians(start_angle + angle / 2)
lx = cx + r * 0.6 * math.cos(mid)
ly = cy + r * 0.6 * math.sin(mid)
label_text = f"{val}/{total}" if total != sum(fractions) else str(val)
paths.append(f'<text x="{lx:.1f}" y="{ly:.1f}" text-anchor="middle" dominant-baseline="central" font-size="14" fill="white" font-weight="bold">{label_text}</text>')
start_angle = end_angle
return f'''<svg viewBox="0 0 {size} {size}" xmlns="http://www.w3.org/2000/svg">
{''.join(paths)}
{"<text x='" + str(cx) + "' y='" + str(size - 8) + "' text-anchor='middle' font-size='13' fill='#333'>" + title + "</text>" if title else ""}
</svg>'''
def number_line(start: int, end: int, highlights: list = None, size: tuple = (500, 80)) -> str:
"""数轴 — 用于加减法、负数、小数"""
w, h = size
margin = 40
line_y = h // 2
usable_w = w - 2 * margin
n_points = end - start + 1
spacing = usable_w / max(n_points - 1, 1)
elements = [f'<line x1="{margin}" y1="{line_y}" x2="{w - margin}" y2="{line_y}" stroke="#333" stroke-width="2"/>']
for i in range(n_points):
val = start + i
x = margin + i * spacing
elements.append(f'<line x1="{x:.0f}" y1="{line_y - 8}" x2="{x:.0f}" y2="{line_y + 8}" stroke="#333" stroke-width="1.5"/>')
elements.append(f'<text x="{x:.0f}" y="{line_y + 25}" text-anchor="middle" font-size="14" fill="#333">{val}</text>')
if highlights:
for hl in highlights:
v = hl.get("value", 0)
label = hl.get("label", "")
x = margin + (v - start) * spacing
elements.append(f'<circle cx="{x:.0f}" cy="{line_y}" r="12" fill="#FF6B6B" opacity="0.7"/>')
if label:
elements.append(f'<text x="{x:.0f}" y="{line_y - 20}" text-anchor="middle" font-size="14" fill="#FF6B6B" font-weight="bold">{label}</text>')
return f'<svg viewBox="0 0 {w} {h}" xmlns="http://www.w3.org/2000/svg">{"".join(elements)}</svg>'
def grid_array(rows: int, cols: int, highlight_cells: list = None, cell_size: int = 40) -> str:
"""阵列/格子 — 用于乘法、面积"""
w = cols * cell_size + 20
h = rows * cell_size + 20
elements = []
highlight_set = set(highlight_cells or [])
for r in range(rows):
for c in range(cols):
x = 10 + c * cell_size
y = 10 + r * cell_size
idx = r * cols + c
color = "#FF6B6B" if idx in highlight_set else "#4ECDC4"
opacity = "0.6" if idx in highlight_set else "0.3"
elements.append(f'<rect x="{x}" y="{y}" width="{cell_size - 2}" height="{cell_size - 2}" fill="{color}" opacity="{opacity}" rx="4"/>')
return f'<svg viewBox="0 0 {w} {h}" xmlns="http://www.w3.org/2000/svg">{"".join(elements)}</svg>'
def bar_chart(values: list, labels: list = None, title: str = "", size: tuple = (400, 250)) -> str:
"""柱状图"""
w, h = size
margin_t, margin_b, margin_l, margin_r = 30, 40, 50, 20
chart_w = w - margin_l - margin_r
chart_h = h - margin_t - margin_b
max_val = max(values) if values else 1
bar_w = chart_w / len(values) * 0.6
gap = chart_w / len(values) * 0.4
elements = [
f'<line x1="{margin_l}" y1="{margin_t}" x2="{margin_l}" y2="{h - margin_b}" stroke="#333" stroke-width="2"/>',
f'<line x1="{margin_l}" y1="{h - margin_b}" x2="{w - margin_r}" y2="{h - margin_b}" stroke="#333" stroke-width="2"/>'
]
colors = ["#FF6B6B", "#4ECDC4", "#45B7D1", "#96CEB4", "#FFEAA7", "#DDA0DD"]
for i, val in enumerate(values):
bar_h = (val / max_val) * chart_h if max_val > 0 else 0
x = margin_l + i * (bar_w + gap) + gap / 2
y = h - margin_b - bar_h
elements.append(f'<rect x="{x:.0f}" y="{y:.0f}" width="{bar_w:.0f}" height="{bar_h:.0f}" fill="{colors[i % len(colors)]}" rx="3" opacity="0.8"/>')
elements.append(f'<text x="{x + bar_w / 2:.0f}" y="{y - 8:.0f}" text-anchor="middle" font-size="13" fill="#333" font-weight="bold">{val}</text>')
if labels and i < len(labels):
elements.append(f'<text x="{x + bar_w / 2:.0f}" y="{h - margin_b + 20:.0f}" text-anchor="middle" font-size="12" fill="#666">{labels[i]}</text>')
if title:
elements.append(f'<text x="{w / 2:.0f}" y="{margin_t - 10:.0f}" text-anchor="middle" font-size="15" fill="#333" font-weight="bold">{title}</text>')
return f'<svg viewBox="0 0 {w} {h}" xmlns="http://www.w3.org/2000/svg">{"".join(elements)}</svg>'
def shapes_library() -> dict:
"""常用几何图形 SVG"""
return {
"square": '<svg viewBox="0 0 100 100"><rect x="10" y="10" width="80" height="80" fill="#4ECDC4" opacity="0.5" stroke="#333" stroke-width="2"/></svg>',
"rectangle": '<svg viewBox="0 0 150 100"><rect x="10" y="15" width="130" height="70" fill="#45B7D1" opacity="0.5" stroke="#333" stroke-width="2"/></svg>',
"triangle": '<svg viewBox="0 0 100 100"><polygon points="50,10 90,85 10,85" fill="#FF6B6B" opacity="0.5" stroke="#333" stroke-width="2"/></svg>',
"circle": '<svg viewBox="0 0 100 100"><circle cx="50" cy="50" r="40" fill="#96CEB4" opacity="0.5" stroke="#333" stroke-width="2"/></svg>',
}