Django Matplotlib SVG 无法调整大小?4 种方法搞定
2025-04-24 18:45:12
好的,这是博客文章内容:
Django 模板里 Matplotlib 生成的 SVG 图,怎么调整大小?
写 Django 应用时,用 Matplotlib 从数据生成图表(比如折线图)挺常见的。生成 SVG 格式可以直接嵌入 HTML 模板,矢量图嘛,放大不失真,挺好。但问题来了,生成的 SVG 直接怼到模板里,想用 CSS 控制它的大小,结果发现父级 div
的尺寸变了,里面的 SVG 图纹丝不动,这是咋回事?
看下代码,视图里大概是这么写的:
import io
import matplotlib.pyplot as plt
# 假设 fig 是已经绘制好的 Matplotlib Figure 对象
def generate_svg(fig):
"""将 Matplotlib Figure 保存为 SVG 字符串"""
imgdata = io.StringIO()
# 注意这里保存时可能没有指定尺寸,或者用了默认尺寸
fig.savefig(imgdata, format='svg')
imgdata.seek(0)
data = imgdata.getvalue()
plt.close(fig) # 处理完最好关闭 figure 释放内存
return data
# Django view 函数示例
def my_view(request):
# ... 获取数据、创建 fig 对象 ...
fig, ax = plt.subplots() # 创建一个示例图
ax.plot([1, 2, 3], [4, 5, 4]) # 画点东西
svg_data = generate_svg(fig)
# 假设 locations 是其他需要传递的数据
locations = ["loc1", "loc2"]
context = {
'data': svg_data,
'locations': locations
}
return render(request, 'my_template.html', context)
模板 my_template.html
里是这样:
<div class="figure">
{{ data|safe }}
</div>
然后配上 CSS:
.figure {
width: 40%; /* 或者其他你想要的尺寸 */
height: auto; /* 高度设为 auto 通常更好,保持 SVG 宽高比 */
border: 1px solid red; /* 加个边框方便观察 div 尺寸 */
}
刷新页面一看,红色的边框框出的 div
确实是 40% 宽度了,但是里面的 SVG 图还是老样子,要多大有多大,完全无视 div
的大小限制。
问题根源在哪?
这事儿吧,根子在 Matplotlib 生成的 SVG 内容上。
用 fig.savefig(imgdata, format='svg')
生成 SVG 时,Matplotlib 会在 SVG 的根元素 <svg>
上添加 width
和 height
属性,通常是以 pt
(points) 为单位的物理尺寸。比如,它生成的 SVG 代码开头可能是这样的:
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
width="432pt" height="288pt" viewBox="0 0 432 288" ...>
<!-- SVG 图形内容 -->
</svg>
看到了吧?width="432pt" height="288pt"
这俩属性把 SVG 的显示尺寸给写死了。
当你把这段 SVG 代码用 {{ data|safe }}
直接插入 HTML 时,浏览器就认这两个写死的 width
和 height
属性。CSS 里给父级 div.figure
设置的 width
和 height
,只能影响 div
本身的大小,管不到里面那个有自己主见(自带宽高属性)的 SVG 元素。除非 SVG 内部有特定的响应式设置(比如 width="100%"
),否则它就按自己的属性来。
咋解决呢?有好几种法子
既然知道了问题原因,解决起来就有方向了。下面列几种常用的方法,你可以根据自己的情况选一个。
方法一:在 Matplotlib 保存时就指定好尺寸
最直接的想法:让 Matplotlib 生成 SVG 时,就别用默认尺寸,指定咱们期望的尺寸。
原理:
控制源头。在调用 savefig
或者创建 Figure
对象时,就告诉 Matplotlib 图片的物理尺寸应该是多少。
操作步骤:
-
创建 Figure 时指定尺寸:
推荐在创建Figure
对象时通过figsize
参数指定尺寸(单位是英寸 inches)。Matplotlib 会根据这个尺寸和dpi
(每英寸点数)来计算最终的像素或点(pt)尺寸。import matplotlib.pyplot as plt import io # 假设你想让图形在页面上大概是 6 英寸宽, 4 英寸高 # DPI 默认为 100 左右,可以根据需要调整 fig, ax = plt.subplots(figsize=(6, 4), dpi=100) ax.plot([1, 2, 3], [4, 5, 4]) # 后续保存逻辑不变 imgdata = io.StringIO() fig.savefig(imgdata, format='svg') # 这里生成的 SVG 内部 width/height 会基于 figsize 和 dpi imgdata.seek(0) data = imgdata.getvalue() plt.close(fig) # 别忘了关闭 # return data ...
-
调整现有 Figure 对象的尺寸:
如果fig
对象已经存在,可以用fig.set_size_inches(width, height)
来调整。# 假设 fig 已经存在 fig.set_size_inches(6, 4) # 设置为 6x4 英寸 # 再保存 imgdata = io.StringIO() fig.savefig(imgdata, format='svg') # ...
说明:
- 这种方法生成的 SVG 还是带有固定的
width
和height
(pt),但这个尺寸是你期望的基础尺寸。 - 它不太适合需要响应式 布局的场景。图片尺寸在生成时就固定了,不会根据浏览器窗口大小变化。
figsize
单位是英寸,SVG 里的pt
和像素的关系还受dpi
影响。需要稍微试一下找到合适的值。
方法二:用 CSS 直接控制 SVG 元素
既然是 CSS 控制不了 SVG 的问题,那我们就让 CSS 能直接选中 SVG 元素本身,然后强行覆盖它的 width
和 height
属性。
原理:
利用 CSS 的层叠和选择器优先级。通过更具体的 CSS 选择器(比如 .figure svg
)来选中 <svg>
元素,然后设置它的 width
和 height
样式,覆盖掉它标签上自带的属性。
操作步骤:
-
修改 CSS:
把 CSS 选择器指向div.figure
里面的svg
元素。.figure { width: 40%; /* 父容器控制宽度 */ height: auto; /* 高度自适应内容,或者你也可以给个固定高度 */ border: 1px solid red; /* 调试用边框 */ } .figure svg { /* 选中容器内的 svg 元素 */ width: 100%; /* 让 SVG 宽度充满父容器 */ height: auto; /* 高度自动,保持 SVG 的原始宽高比 */ display: block; /* 消除可能的底部空白,可选 */ }
说明:
width: 100%; height: auto;
是实现响应式 SVG 的关键。SVG 会自动缩放以适应其容器的宽度,同时保持自身的宽高比。display: block;
有时候可以去掉 SVG 作为 inline 元素时可能产生的下方微小空隙。- 这种方法非常灵活,推荐用于需要 SVG 尺寸随页面布局变化的场景。
- 浏览器对 SVG 的 CSS 支持非常好,兼容性一般没问题。
- SVG 文件本身依然包含原始的
width
和height
(pt) 属性,但这通常不影响 CSS 的覆盖效果。重要的是viewBox
属性,Matplotlib 一般会自动加上,viewBox
定义了 SVG 的内部坐标系和宽高比,是实现缩放的基础。
方法三:后端 Python 处理 SVG 字符串
如果不想依赖纯 CSS,或者想对 SVG 做更多处理(比如移除脚本、清理元数据等),可以在 Django视图里,拿到 SVG 字符串后,用 Python 处理一下,再传给模板。
原理:
在 SVG 字符串发送到前端之前,用 Python 的字符串处理或 XML 解析库,找到 <svg>
标签,修改或删除 width
和 height
属性,可能还会检查或添加 viewBox
属性。
操作步骤:
-
使用正则表达式 (简单粗暴):
如果 SVG 结构比较稳定,可以用正则替换掉width
和height
属性。import re import io # ... generate_svg(fig) 拿到原始 svg_data ... def adjust_svg_size_regex(svg_string): # 移除 width 和 height 属性 # 注意:这个正则比较简单,可能不够健壮,比如属性顺序变了或者值里有特殊字符 svg_string = re.sub(r'width="[^"]+"', '', svg_string, count=1) svg_string = re.sub(r'height="[^"]+"', '', svg_string, count=1) # 或者,直接添加 style 属性 (如果你想用 style 控制) # svg_string = re.sub(r'(<svg[^>]*?)>', r'\1 style="width:100%;height:auto;">', svg_string, count=1) # 注意上面的正则直接添加 style,可能覆盖原有 style # 更好的做法可能是移除 width/height,让 CSS 控制 (如方法二) # 确保 viewBox 存在,如果不存在可能需要添加 (Matplotlib 通常会加) return svg_string # 在 view 中调用 svg_data = generate_svg(fig) adjusted_svg_data = adjust_svg_size_regex(svg_data) context = {'data': adjusted_svg_data, ...} # return render(request, 'my_template.html', context)
-
使用 XML 解析库 (更稳妥):
推荐使用xml.etree.ElementTree
(Python 内置) 或lxml
(功能更强,需安装) 来解析和修改 SVG。import xml.etree.ElementTree as ET import io # ... generate_svg(fig) 拿到原始 svg_data ... def adjust_svg_size_xml(svg_string): try: # lxml 对命名空间处理更好,但这里用内置库示例 # ElementTree 对 SVG 的默认命名空间处理可能需要点技巧 # 尝试注册命名空间 namespaces = {'svg': 'http://www.w3.org/2000/svg'} ET.register_namespace('', namespaces['svg']) # 注册默认命名空间 # 解析 SVG 字符串 # 编码问题要注意,Matplotlib 输出默认是 utf-8 svg_root = ET.fromstring(svg_string.encode('utf-8')) # 移除 width 和 height 属性 if 'width' in svg_root.attrib: del svg_root.attrib['width'] if 'height' in svg_root.attrib: del svg_root.attrib['height'] # 确保存有 viewBox,这是缩放的关键 if 'viewBox' not in svg_root.attrib: # 如果 Matplotlib 没加 viewBox,可能需要根据 width/height 计算一个 # 这里假设它总是有,如果没有,逻辑会更复杂 print("Warning: SVG might be missing viewBox, scaling may not work correctly.") pass # 这里应该有更健壮的处理逻辑 # 可选:添加 CSS class 或 style # svg_root.set('class', 'resizable-svg') # 方便 CSS 选择 # svg_root.set('style', 'width:100%; height:auto;') # 直接加内联样式 # 将修改后的 Element 对象转回字符串 # xml_declaration=True 会加上 <?xml ...?> 头 # encoding='unicode' 得到字符串,而不是 bytes adjusted_svg_string = ET.tostring(svg_root, encoding='unicode', xml_declaration=False) return adjusted_svg_string except ET.ParseError as e: print(f"Error parsing SVG: {e}") return svg_string # 解析失败返回原始字符串 # 在 view 中调用 svg_data = generate_svg(fig) adjusted_svg_data = adjust_svg_size_xml(svg_data) context = {'data': adjusted_svg_data, ...} # return render(request, 'my_template.html', context)
安全建议:
- 用正则处理 XML/HTML 有风险,结构稍有变化就可能失效。XML 解析库更可靠。
- 虽然这里 SVG 是自己生成的,但如果将来 SVG 来源不可信,用 XML 解析库还能顺便做些安全过滤(比如移除
<script>
标签)。
进阶技巧:
- 除了移除
width
/height
,还可以检查并修正viewBox
属性。viewBox="0 0 w h"
定义了 SVG 的内部坐标系,w
和h
通常对应原始的宽和高。有了viewBox
,设置width="100%"
height="auto"
才能正确缩放。 - 用 XML 库可以做更多操作,比如给 SVG 元素或内部图形添加 ID、类名,方便前端交互或更精细的样式控制。
方法四:用 <img>
或 <object>
标签加载 SVG
不直接把 SVG 代码插到 HTML 里,而是把它当作一个独立的资源来加载。
原理:
将 SVG 数据转换成 Data URI 或者创建一个专门返回 SVG 的 Django 视图 URL,然后在 HTML 中使用 <img src="...">
或 <object data="...">
标签来引用这个 SVG 资源。这样就可以像对待普通图片一样用 CSS 控制 <img>
或 <object>
标签的大小了。
操作步骤:
-
使用 Data URI (适用于较小的 SVG):
在 Django 视图里,把 SVG 字符串编码成 Data URI 格式。import base64 import urllib.parse import io # ... generate_svg(fig) 拿到原始 svg_data ... def svg_to_data_uri(svg_string): # 方式一:直接 URL 编码 SVG 内容 (更推荐,效率高) encoded_svg = urllib.parse.quote(svg_string) return f"data:image/svg+xml,{encoded_svg}" # 方式二:Base64 编码 (会增大体积约 33%) # encoded_svg = base64.b64encode(svg_string.encode('utf-8')).decode('utf-8') # return f"data:image/svg+xml;base64,{encoded_svg}" # 在 view 中 svg_data = generate_svg(fig) svg_uri = svg_to_data_uri(svg_data) context = {'svg_uri': svg_uri, ...} # return render(request, 'my_template.html', context)
模板里这样用:
<div class="figure"> {# 使用 img 标签 #} <img src="{{ svg_uri }}" alt="Matplotlib Figure"> {# 或者使用 object 标签 #} {# <object type="image/svg+xml" data="{{ svg_uri }}"> Your browser does not support SVG </object> #} </div>
CSS 就直接控制
img
或object
元素:.figure img, /* 或者 .figure object */ .figure object { width: 100%; height: auto; display: block; }
-
使用专门的 Django 视图返回 SVG:
创建一个新的 Django 视图,这个视图只负责接收参数(如果需要,比如图形类型、数据源 ID),生成 SVG,然后返回一个带有正确Content-Type
(image/svg+xml
) 的HttpResponse
。urls.py
添加路由:from django.urls import path from . import views urlpatterns = [ # ... other paths path('plot/svg/<str:some_identifier>/', views.serve_svg_plot, name='serve_svg_plot'), ]
views.py
添加 SVG 视图:from django.http import HttpResponse # ... import generate_svg, plt, etc. ... def serve_svg_plot(request, some_identifier): # 根据 some_identifier 获取数据、生成 fig 对象 # 这部分逻辑可能需要重构复用 fig, ax = plt.subplots(figsize=(6, 4)) # 尺寸在这里控制或后续处理 ax.plot([1, 2, 3], [4, 5, 4]) # 示例 svg_data = generate_svg(fig) # 可选:在这里用方法三处理 svg_data 字符串 return HttpResponse(svg_data, content_type='image/svg+xml') # 原来的主视图就不需要生成 SVG 数据了,只需要生成 URL def my_view(request): some_id = "unique_plot_id" # 根据实际情况生成标识符 plot_url = reverse('serve_svg_plot', kwargs={'some_identifier': some_id}) locations = ["loc1", "loc2"] context = { 'plot_url': plot_url, 'locations': locations } return render(request, 'my_template.html', context)
- 模板
my_template.html
里使用 URL:<div class="figure"> <img src="{{ plot_url }}" alt="Matplotlib Figure"> {# 或者 <object type="image/svg+xml" data="{{ plot_url }}"></object> #} </div>
- CSS 不变,还是控制
.figure img
或.figure object
。
说明:
- Data URI : 简单直接,不需要额外请求。缺点是 SVG 内容直接塞进 HTML,如果 SVG 很大,HTML 文件会很臃肿;浏览器对 Data URI 长度也有限制(虽然通常很高)。不利用浏览器缓存。
- 专用视图 URL : 更清晰分离。SVG 作为独立资源可以被浏览器缓存。适合较大的或需要动态生成的 SVG。缺点是多了一次 HTTP 请求(不过缓存可以缓解)。
<img>
vs<object>
:<img>
最简单,像用普通图片一样。但用<img>
加载的 SVG,不能用 CSS 去修改 SVG 内部元素的样式,也不能用 JavaScript 和 SVG 内部交互。纯粹是张图片。<object>
更强大,它嵌入的是一个独立的文档。可以用 CSS/JS 和 SVG 内部交互(需要注意同源策略等)。但也稍微复杂一点。
- 对于纯粹显示、调整大小的需求,
<img>
配合 Data URI 或专用视图 URL 通常足够,也最容易上手。
选哪个好?
- 如果 SVG 尺寸固定,不需要响应式,方法一(调整 Matplotlib 输出) 最省事。
- 如果需要 SVG 随容器响应式缩放,方法二(纯 CSS 控制) 是最常用、最灵活的方式。
- 如果除了调整尺寸,还想对 SVG 做清理、修改内部结构,或者想确保移除
width
/height
让 CSS 完全接管,方法三(后端处理 SVG 字符串) 更可控,用 XML 解析库最稳。 - 如果想把 SVG 当作标准资源来管理(缓存、单独访问),或者在 HTML 里保持简洁,方法四(
<img>
或<object>
加载) 是个好选择,特别是用专用视图 URL 的方式。
通常情况下,推荐优先考虑 方法二 (纯 CSS 控制) ,因为它直接、有效,且符合 Web 开发中内容与表现分离的原则。如果 CSS 不生效或想做得更彻底,可以结合 方法三 (后端移除 width/height) 。如果项目结构复杂或对资源管理有要求,方法四 (专用视图) 也不错。根据实际需求和偏好来选就行。