返回

Django Matplotlib SVG 无法调整大小?4 种方法搞定

python

好的,这是博客文章内容:

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> 上添加 widthheight 属性,通常是以 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 时,浏览器就认这两个写死的 widthheight 属性。CSS 里给父级 div.figure 设置的 widthheight,只能影响 div 本身的大小,管不到里面那个有自己主见(自带宽高属性)的 SVG 元素。除非 SVG 内部有特定的响应式设置(比如 width="100%"),否则它就按自己的属性来。

咋解决呢?有好几种法子

既然知道了问题原因,解决起来就有方向了。下面列几种常用的方法,你可以根据自己的情况选一个。

方法一:在 Matplotlib 保存时就指定好尺寸

最直接的想法:让 Matplotlib 生成 SVG 时,就别用默认尺寸,指定咱们期望的尺寸。

原理:
控制源头。在调用 savefig 或者创建 Figure 对象时,就告诉 Matplotlib 图片的物理尺寸应该是多少。

操作步骤:

  1. 创建 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 ...
    
  2. 调整现有 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 还是带有固定的 widthheight (pt),但这个尺寸是你期望的基础尺寸。
  • 它不太适合需要响应式 布局的场景。图片尺寸在生成时就固定了,不会根据浏览器窗口大小变化。
  • figsize 单位是英寸,SVG 里的 pt 和像素的关系还受 dpi 影响。需要稍微试一下找到合适的值。

方法二:用 CSS 直接控制 SVG 元素

既然是 CSS 控制不了 SVG 的问题,那我们就让 CSS 能直接选中 SVG 元素本身,然后强行覆盖它的 widthheight 属性。

原理:
利用 CSS 的层叠和选择器优先级。通过更具体的 CSS 选择器(比如 .figure svg)来选中 <svg> 元素,然后设置它的 widthheight 样式,覆盖掉它标签上自带的属性。

操作步骤:

  1. 修改 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 文件本身依然包含原始的 widthheight (pt) 属性,但这通常不影响 CSS 的覆盖效果。重要的是 viewBox 属性,Matplotlib 一般会自动加上,viewBox 定义了 SVG 的内部坐标系和宽高比,是实现缩放的基础。

方法三:后端 Python 处理 SVG 字符串

如果不想依赖纯 CSS,或者想对 SVG 做更多处理(比如移除脚本、清理元数据等),可以在 Django视图里,拿到 SVG 字符串后,用 Python 处理一下,再传给模板。

原理:
在 SVG 字符串发送到前端之前,用 Python 的字符串处理或 XML 解析库,找到 <svg> 标签,修改或删除 widthheight 属性,可能还会检查或添加 viewBox 属性。

操作步骤:

  1. 使用正则表达式 (简单粗暴):
    如果 SVG 结构比较稳定,可以用正则替换掉 widthheight 属性。

    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)
    
    
  2. 使用 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 的内部坐标系,wh 通常对应原始的宽和高。有了 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> 标签的大小了。

操作步骤:

  1. 使用 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 就直接控制 imgobject 元素:

    .figure img, /* 或者 .figure object */
    .figure object {
      width: 100%;
      height: auto;
      display: block;
    }
    
  2. 使用专门的 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) 。如果项目结构复杂或对资源管理有要求,方法四 (专用视图) 也不错。根据实际需求和偏好来选就行。