马一丁

Offline JS Library

@@ -8,6 +8,8 @@ import ast @@ -8,6 +8,8 @@ import ast
8 import copy 8 import copy
9 import html 9 import html
10 import json 10 import json
  11 +import os
  12 +from pathlib import Path
11 from typing import Any, Dict, List 13 from typing import Any, Dict, List
12 14
13 15
@@ -61,6 +63,38 @@ class HTMLRenderer: @@ -61,6 +63,38 @@ class HTMLRenderer:
61 self.secondary_heading_index = 0 63 self.secondary_heading_index = 0
62 self.toc_rendered = False 64 self.toc_rendered = False
63 self.hero_kpi_signature: tuple | None = None 65 self.hero_kpi_signature: tuple | None = None
  66 + self._lib_cache: Dict[str, str] = {}
  67 +
  68 + @staticmethod
  69 + def _get_lib_path() -> Path:
  70 + """获取第三方库文件的目录路径"""
  71 + return Path(__file__).parent / "libs"
  72 +
  73 + def _load_lib(self, filename: str) -> str:
  74 + """
  75 + 加载指定的第三方库文件内容
  76 +
  77 + 参数:
  78 + filename: 库文件名
  79 +
  80 + 返回:
  81 + str: 库文件的JavaScript代码内容
  82 + """
  83 + if filename in self._lib_cache:
  84 + return self._lib_cache[filename]
  85 +
  86 + lib_path = self._get_lib_path() / filename
  87 + try:
  88 + with open(lib_path, 'r', encoding='utf-8') as f:
  89 + content = f.read()
  90 + self._lib_cache[filename] = content
  91 + return content
  92 + except FileNotFoundError:
  93 + print(f"警告: 库文件 {filename} 未找到,将使用CDN备用链接")
  94 + return ""
  95 + except Exception as e:
  96 + print(f"警告: 读取库文件 {filename} 时出错: {e}")
  97 + return ""
64 98
65 # ====== 公共入口 ====== 99 # ====== 公共入口 ======
66 100
@@ -147,16 +181,31 @@ class HTMLRenderer: @@ -147,16 +181,31 @@ class HTMLRenderer:
147 str: head片段HTML。 181 str: head片段HTML。
148 """ 182 """
149 css = self._build_css(theme_tokens) 183 css = self._build_css(theme_tokens)
  184 +
  185 + # 加载第三方库
  186 + chartjs = self._load_lib("chart.js")
  187 + chartjs_sankey = self._load_lib("chartjs-chart-sankey.js")
  188 + html2canvas = self._load_lib("html2canvas.min.js")
  189 + jspdf = self._load_lib("jspdf.umd.min.js")
  190 + mathjax = self._load_lib("mathjax.js")
  191 +
  192 + # 如果库文件加载失败,使用CDN备用链接
  193 + chartjs_tag = f"<script>{chartjs}</script>" if chartjs else '<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>'
  194 + sankey_tag = f"<script>{chartjs_sankey}</script>" if chartjs_sankey else '<script src="https://cdn.jsdelivr.net/npm/chartjs-chart-sankey@4"></script>'
  195 + html2canvas_tag = f"<script>{html2canvas}</script>" if html2canvas else '<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>'
  196 + jspdf_tag = f"<script>{jspdf}</script>" if jspdf else '<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>'
  197 + mathjax_tag = f"<script defer>{mathjax}</script>" if mathjax else '<script defer src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>'
  198 +
150 return f""" 199 return f"""
151 <head> 200 <head>
152 <meta charset="utf-8" /> 201 <meta charset="utf-8" />
153 <meta http-equiv="X-UA-Compatible" content="IE=edge" /> 202 <meta http-equiv="X-UA-Compatible" content="IE=edge" />
154 <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 203 <meta name="viewport" content="width=device-width, initial-scale=1.0" />
155 <title>{self._escape_html(title)}</title> 204 <title>{self._escape_html(title)}</title>
156 - <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>  
157 - <script src="https://cdn.jsdelivr.net/npm/chartjs-chart-sankey@4"></script>  
158 - <script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>  
159 - <script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script> 205 + {chartjs_tag}
  206 + {sankey_tag}
  207 + {html2canvas_tag}
  208 + {jspdf_tag}
160 <script> 209 <script>
161 window.MathJax = {{ 210 window.MathJax = {{
162 tex: {{ 211 tex: {{
@@ -169,7 +218,7 @@ class HTMLRenderer: @@ -169,7 +218,7 @@ class HTMLRenderer:
169 }} 218 }}
170 }}; 219 }};
171 </script> 220 </script>
172 - <script defer src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script> 221 + {mathjax_tag}
173 <style> 222 <style>
174 {css} 223 {css}
175 </style> 224 </style>
  1 +# 第三方JavaScript库
  2 +
  3 +本目录包含HTML报告渲染所需的第三方JavaScript库。这些库已经被内联到生成的HTML文件中,以便在离线环境中使用。
  4 +
  5 +## 包含的库
  6 +
  7 +1. **chart.js** (204KB) - 用于图表渲染
  8 + - 版本: 4.5.1
  9 + - 来源: https://cdn.jsdelivr.net/npm/chart.js
  10 +
  11 +2. **chartjs-chart-sankey.js** (10KB) - Sankey图表插件
  12 + - 版本: 0.12.0
  13 + - 来源: https://unpkg.com/chartjs-chart-sankey@0.12.0/dist/chartjs-chart-sankey.min.js
  14 +
  15 +3. **html2canvas.min.js** (194KB) - HTML转Canvas工具
  16 + - 版本: 1.4.1
  17 + - 来源: https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js
  18 +
  19 +4. **jspdf.umd.min.js** (356KB) - PDF导出库
  20 + - 版本: 2.5.1
  21 + - 来源: https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js
  22 +
  23 +5. **mathjax.js** (1.1MB) - 数学公式渲染引擎
  24 + - 版本: 3.2.2
  25 + - 来源: https://cdn.jsdelivr.net/npm/mathjax@3.2.2/es5/tex-mml-chtml.js
  26 +
  27 +## 功能说明
  28 +
  29 +HTML渲染器(`html_renderer.py`)会自动从本目录加载这些库文件,并将它们内联到生成的HTML中。这样做有以下优点:
  30 +
  31 +- ✅ 离线环境可用 - 无需网络连接即可正常显示报告
  32 +- ✅ 加载速度快 - 不依赖外部CDN
  33 +- ✅ 稳定性高 - 不受CDN服务中断影响
  34 +- ✅ 版本固定 - 确保功能的一致性
  35 +
  36 +## 备用机制
  37 +
  38 +如果库文件加载失败(如文件不存在或读取错误),渲染器会自动回退到使用CDN链接,确保在任何情况下都能正常工作。
  39 +
  40 +## 更新库文件
  41 +
  42 +如需更新库文件,请:
  43 +
  44 +1. 从相应的CDN下载最新版本
  45 +2. 替换本目录中的对应文件
  46 +3. 更新本README文件中的版本信息
  47 +
  48 +## 注意事项
  49 +
  50 +- 总大小约为1.86MB,会增加生成的HTML文件大小
  51 +- 对于不需要图表和数学公式的简单报告,这些库仍然会被包含
  52 +- 如果需要减小文件大小,可以考虑使用更轻量的替代方案
This diff could not be displayed because it is too large.
  1 +/*!
  2 + * chartjs-chart-sankey v0.12.0
  3 + * https://github.com/kurkle/chartjs-chart-sankey#readme
  4 + * (c) 2022 Jukka Kurkela
  5 + * Released under the MIT license
  6 + */
  7 +!function(t,o){"object"==typeof exports&&"undefined"!=typeof module?o(require("chart.js"),require("chart.js/helpers")):"function"==typeof define&&define.amd?define(["chart.js","chart.js/helpers"],o):o((t="undefined"!=typeof globalThis?globalThis:t||self).Chart,t.Chart.helpers)}(this,(function(t,o){"use strict";function e(t){return t&&-1!==["min","max"].indexOf(t)?t:"max"}const r=t=>void 0!==t;function n(t,o){const e=t.filter((t=>!o.has(t)));return e.length?e:t.slice(0,1)}const a=(t,o)=>t.x!==o.x?t.x-o.x:t.y-o.y;let i=-1;function s(t,o,e=function(){return i=i<100?i+1:0,i}()){let r=0;for(const n of t)n.node._visited!==e&&(n.node._visited=e,r+=n.node[o].length+s(n.node[o],o,e));return r}const l=t=>(o,e)=>s(o.node[t],t)-s(e.node[t],t)||o.node[t].length-e.node[t].length;function c(t,o){t.from.sort(l("from"));for(const e of t.from){const t=e.node;r(t.y)||(t.y=o,c(t,o)),o=Math.max(t.y+t.out,o)}return o}function h(t,o){t.to.sort(l("to"));for(const e of t.to){const t=e.node;r(t.y)||(t.y=o,h(t,o)),o=Math.max(t.y+t.in,o)}return o}function f(t,o){return r(t.y)?t.y:(t.y=o,o)}function d(t,o){t.sort(((t,o)=>Math.max(o.in,o.out)-Math.max(t.in,t.out)));const e=t[0];e.y=0;const n=c(e,0),a=h(e,0),i=function(t,o){const e=t.filter((t=>0===t.x)),n=t.filter((t=>t.x===o)),a=e.filter((t=>!r(t.y))),i=n.filter((t=>!r(t.y))),s=t.filter((t=>t.x>0&&t.x<o&&!r(t.y)));let l=e.reduce(((t,o)=>Math.max(t,o.y+o.out||0)),0),d=n.reduce(((t,o)=>Math.max(t,o.y+o.in||0)),0),u=0;return l>=d?(a.forEach((t=>{l=f(t,l),l=Math.max(l+t.out,h(t,l))})),i.forEach((t=>{d=f(t,d),d=Math.max(d+t.in,h(t,d))}))):(i.forEach((t=>{d=f(t,d),d=Math.max(d+t.in,h(t,d))})),a.forEach((t=>{l=f(t,l),l=Math.max(l+t.out,h(t,l))}))),s.forEach((o=>{let e=t.filter((t=>t.x===o.x&&r(t.y))).reduce(((t,o)=>Math.max(t,o.y+Math.max(o.in,o.out))),0);e=f(o,e),e=Math.max(e+o.in,c(o,e)),e=Math.max(e+o.out,h(o,e)),u=Math.max(u,e)})),Math.max(l,d,u)}(t,o);return Math.max(n,a,i)}function u(t,o,e,i){const s=[...t.values()],l=function(t,o){const e=new Set(o.map((t=>t.to))),a=new Set(o.map((t=>t.from))),i=new Set([...t.keys()]);let s=0;for(;i.size;){const a=n([...i],e);for(const o of a){const e=t.get(o);r(e.x)||(e.x=s),i.delete(o)}i.size&&(e.clear(),o.filter((t=>i.has(t.from))).forEach((t=>e.add(t.to))),s++)}return[...t.keys()].filter((t=>!a.has(t))).forEach((o=>{const e=t.get(o);e.column||(e.x=s)})),s}(t,o),c=e?function(t,o){let e=0,r=0;for(let n=0;n<=o;n++){let o=r;const a=t.filter((t=>t.x===n)).sort(((t,o)=>t.priority-o.priority));r=a[0].to.filter((t=>t.node.x>n+1)).reduce(((t,o)=>t+o.flow),0)||0;for(const t of a)t.y=o,o+=Math.max(t.out,t.in);e=Math.max(o,e)}return e}(s,l):d(s,l),h=function(t,o){let e=1,r=0,n=0,i=0;const s=[];t.sort(a);for(const a of t){if(a.y){if(0===a.x)s.push(a.y);else{for(r!==a.x&&(r=a.x,n=0),e=n+1;e<s.length&&!(s[e]>a.y);e++);n=e}a.y+=e*o,e++}i=Math.max(i,a.y+Math.max(a.in,a.out))}return i}(s,.03*c);return function(t,o){t.forEach((t=>{const e=Math[o](t.in||t.out,t.out||t.in),r=e<t.in,n=e<t.out;let a=0,i=t.from.length;t.from.sort(((t,o)=>t.node.y+t.node.out/2-(o.node.y+o.node.out/2))).forEach(((t,o)=>{r?t.addY=o*(e-t.flow)/(i-1):(t.addY=a,a+=t.flow)})),a=0,i=t.to.length,t.to.sort(((t,o)=>t.node.y+t.node.in/2-(o.node.y+o.node.in/2))).forEach(((t,o)=>{n?t.addY=o*(e-t.flow)/(i-1):(t.addY=a,a+=t.flow)}))}))}(s,i),{maxX:l,maxY:h}}function x(t,o,e){for(const r of t)if(r.key===o&&r.index===e)return r.addY;return 0}class y extends t.DatasetController{parseObjectData(t,o,r,n){const{from:a="from",to:i="to",flow:s="flow"}=this.options.parsing,l=o.map((({[a]:t,[i]:o,[s]:e})=>({from:t,to:o,flow:e}))),{xScale:c,yScale:h}=t,f=[],d=this._nodes=function(t){const o=new Map;for(let e=0;e<t.length;e++){const{from:r,to:n,flow:a}=t[e];if(o.has(r)){const t=o.get(r);t.out+=a,t.to.push({key:n,flow:a,index:e})}else o.set(r,{key:r,in:0,out:a,from:[],to:[{key:n,flow:a,index:e}]});if(o.has(n)){const t=o.get(n);t.in+=a,t.from.push({key:r,flow:a,index:e})}else o.set(n,{key:n,in:a,out:0,from:[{key:r,flow:a,index:e}],to:[]})}const e=(t,o)=>o.flow-t.flow;return[...o.values()].forEach((t=>{t.from=t.from.sort(e),t.from.forEach((t=>{t.node=o.get(t.key)})),t.to=t.to.sort(e),t.to.forEach((t=>{t.node=o.get(t.key)}))})),o}(l),{column:y,priority:p,size:m}=this.getDataset();if(p)for(const t of d.values())t.key in p&&(t.priority=p[t.key]);if(y)for(const t of d.values())t.key in y&&(t.column=!0,t.x=y[t.key]);const{maxX:g,maxY:M}=u(d,l,!!p,e(m));this._maxX=g,this._maxY=M;for(let t=0,o=l.length;t<o;++t){const o=l[t],e=d.get(o.from),r=d.get(o.to),n=e.y+x(e.to,o.to,t),a=r.y+x(r.from,o.from,t);f.push({x:c.parse(e.x,t),y:h.parse(n,t),_custom:{from:e,to:r,x:c.parse(r.x,t),y:h.parse(a,t),height:h.parse(o.flow,t)}})}return f.slice(r,r+n)}getMinMax(t){return{min:0,max:t===this._cachedMeta.xScale?this._maxX:this._maxY}}update(t){const{data:o}=this._cachedMeta;this.updateElements(o,0,o.length,t)}updateElements(t,e,r,n){const{xScale:a,yScale:i}=this._cachedMeta,s=this.resolveDataElementOptions(e,n),l=this.getSharedOptions(n,t[e],s),c=this.getDataset(),h=o.valueOrDefault(c.borderWidth,1)/2+.5,f=o.valueOrDefault(c.nodeWidth,10);for(let o=e;o<e+r;o++){const e=this.getParsed(o),r=e._custom,s=i.getPixelForValue(e.y);this.updateElement(t[o],o,{x:a.getPixelForValue(e.x)+f+h,y:s,x2:a.getPixelForValue(r.x)-h,y2:i.getPixelForValue(r.y),from:r.from,to:r.to,progress:"reset"===n?0:1,height:Math.abs(i.getPixelForValue(e.y+r.height)-s),options:this.resolveDataElementOptions(o,n)},n)}this.updateSharedOptions(l,n)}_drawLabels(){const t=this._ctx,r=this._nodes||new Map,n=this.getDataset(),a=e(n.size),i=o.valueOrDefault(n.borderWidth,1),s=o.valueOrDefault(n.nodeWidth,10),l=n.labels,{xScale:c,yScale:h}=this._cachedMeta;t.save();const f=this.chart.chartArea;for(const o of r.values()){const e=c.getPixelForValue(o.x),r=h.getPixelForValue(o.y),d=Math[a](o.in||o.out,o.out||o.in),u=Math.abs(h.getPixelForValue(o.y+d)-r),x=l&&l[o.key]||o.key;let y=e;t.fillStyle=n.color||"black",t.textBaseline="middle",e<f.width/2?(t.textAlign="left",y+=s+i+4):(t.textAlign="right",y-=i+4),this._drawLabel(x,r,u,t,y)}t.restore()}_drawLabel(t,e,r,n,a){const i=o.toFont(this.options.font,this.chart.options.font),s=o.isNullOrUndef(t)?[]:function(t){const e=[],r=o.isArray(t)?t:o.isNullOrUndef(t)?[]:[t];for(;r.length;){const t=r.pop();"string"==typeof t?e.unshift.apply(e,t.split("\n")):Array.isArray(t)?r.push.apply(r,t):o.isNullOrUndef(r)||e.unshift(""+t)}return e}(t),l=s.length,c=e+r/2,h=i.lineHeight,f=o.valueOrDefault(this.options.padding,h/2);if(n.font=i.string,l>1){const t=c-h*l/2+f;for(let o=0;o<l;o++)n.fillText(s[o],a,t+o*h)}else n.fillText(t,a,c)}_drawNodes(){const t=this._ctx,r=this._nodes||new Map,n=this.getDataset(),a=e(n.size),{xScale:i,yScale:s}=this._cachedMeta,l=o.valueOrDefault(n.borderWidth,1),c=o.valueOrDefault(n.nodeWidth,10);t.save(),t.strokeStyle=n.borderColor||"black",t.lineWidth=l;for(const o of r.values()){t.fillStyle=o.color;const e=i.getPixelForValue(o.x),r=s.getPixelForValue(o.y),n=Math[a](o.in||o.out,o.out||o.in),h=Math.abs(s.getPixelForValue(o.y+n)-r);l&&t.strokeRect(e,r,c,h),t.fillRect(e,r,c,h)}t.restore()}draw(){const t=this._ctx,o=this.getMeta().data||[],e=[];for(let t=0,r=o.length;t<r;++t){const r=o[t];r.from.color=r.options.colorFrom,r.to.color=r.options.colorTo,r.active&&e.push(r)}for(const t of e)t.from.color=t.options.colorFrom,t.to.color=t.options.colorTo;this._drawNodes();for(let e=0,r=o.length;e<r;++e)o[e].draw(t);this._drawLabels()}}y.id="sankey",y.defaults={dataElementType:"flow",animations:{numbers:{type:"number",properties:["x","y","x2","y2","height"]},progress:{easing:"linear",duration:t=>"data"===t.type?200*(t.parsed._custom.x-t.parsed.x):void 0,delay:t=>"data"===t.type?500*t.parsed.x+20*t.dataIndex:void 0},colors:{type:"color",properties:["colorFrom","colorTo"]}},transitions:{hide:{animations:{colors:{type:"color",properties:["colorFrom","colorTo"],to:"transparent"}}},show:{animations:{colors:{type:"color",properties:["colorFrom","colorTo"],from:"transparent"}}}}},y.overrides={interaction:{mode:"nearest",intersect:!0},datasets:{clip:!1,parsing:!0},plugins:{tooltip:{callbacks:{title:()=>"",label(t){const o=t.dataset.data[t.dataIndex];return o.from+" -> "+o.to+": "+o.flow}}},legend:{display:!1}},scales:{x:{type:"linear",bounds:"data",display:!1,min:0,offset:!1},y:{type:"linear",bounds:"data",display:!1,min:0,reverse:!0,offset:!1}},layout:{padding:{top:3,left:3,right:13,bottom:3}}};const p=(t,o,e,r)=>t<e?{cp1:{x:t+(e-t)/3*2,y:o},cp2:{x:t+(e-t)/3,y:r}}:{cp1:{x:t-(t-e)/3,y:0},cp2:{x:e+(t-e)/3,y:0}},m=(t,o,e)=>({x:t.x+e*(o.x-t.x),y:t.y+e*(o.y-t.y)});class g extends t.Element{constructor(t){super(),this.options=void 0,this.x=void 0,this.y=void 0,this.x2=void 0,this.y2=void 0,this.height=void 0,t&&Object.assign(this,t)}draw(t){const{x:e,x2:r,y:n,y2:a,height:i,progress:s}=this,{cp1:l,cp2:c}=p(e,n,r,a);0!==s&&(t.save(),s<1&&(t.beginPath(),t.rect(e,Math.min(n,a),(r-e)*s+1,Math.abs(a-n)+i+1),t.clip()),function(t,{x:e,x2:r,options:n}){let a;"from"===n.colorMode?a=o.color(n.colorFrom).alpha(.5).rgbString():"to"===n.colorMode?a=o.color(n.colorTo).alpha(.5).rgbString():(a=t.createLinearGradient(e,0,r,0),a.addColorStop(0,o.color(n.colorFrom).alpha(.5).rgbString()),a.addColorStop(1,o.color(n.colorTo).alpha(.5).rgbString())),t.fillStyle=a,t.strokeStyle=a,t.lineWidth=.5}(t,this),t.beginPath(),t.moveTo(e,n),t.bezierCurveTo(l.x,l.y,c.x,c.y,r,a),t.lineTo(r,a+i),t.bezierCurveTo(c.x,c.y+i,l.x,l.y+i,e,n+i),t.lineTo(e,n),t.stroke(),t.closePath(),t.fill(),t.restore())}inRange(t,o,e){const{x:r,y:n,x2:a,y2:i,height:s}=this.getProps(["x","y","x2","y2","height"],e);if(t<r||t>a)return!1;const{cp1:l,cp2:c}=p(r,n,a,i),h=(t-r)/(a-r),f={x:a,y:i},d=m({x:r,y:n},l,h),u=m(l,c,h),x=m(c,f,h),y=m(d,u,h),g=m(u,x,h),M=m(y,g,h).y;return o>=M&&o<=M+s}inXRange(t,o){const{x:e,x2:r}=this.getProps(["x","x2"],o);return t>=e&&t<=r}inYRange(t,o){const{y:e,y2:r,height:n}=this.getProps(["y","y2","height"],o),a=Math.min(e,r),i=Math.max(e,r)+n;return t>=a&&t<=i}getCenterPoint(t){const{x:o,y:e,x2:r,y2:n,height:a}=this.getProps(["x","y","x2","y2","height"],t);return{x:(o+r)/2,y:(e+n+a)/2}}tooltipPosition(t){return this.getCenterPoint(t)}getRange(t){return"x"===t?this.width/2:this.height/2}}g.id="flow",g.defaults={colorFrom:"red",colorTo:"green",colorMode:"gradient",hoverColorFrom:(t,e)=>o.getHoverColor(e.colorFrom),hoverColorTo:(t,e)=>o.getHoverColor(e.colorTo)},t.Chart.register(y,g)}));
This diff could not be displayed because it is too large.
This diff could not be displayed because it is too large.
This diff could not be displayed because it is too large.