将 dc.js 图表从 SVG 导出到 PNG

Posted

技术标签:

【中文标题】将 dc.js 图表从 SVG 导出到 PNG【英文标题】:Exporting dc.js chart from SVG to PNG 【发布时间】:2018-07-11 09:31:23 【问题描述】:

我有一个 dc.js 图表,我想将其导出为 PNG 图像,使用 exupero's saveSvgAsPng:

function save() 
  var options = ;
  options.backgroundColor = '#ffffff';
  options.selectorRemap = function(s)  return s.replace(/\.dc-chart/g, ''); ;
  var chart = document.getElementById('chart').getElementsByTagName('svg')[0];
  saveSvgAsPng(chart, 'chart.png', options)


var data = [
  day: 1, service: 'ABC', count: 100,
  day: 2, service: 'ABC', count: 80,
  day: 4, service: 'ABC', count: 10,
  day: 7, service: 'XYZ', count: 380,
  day: 8, service: 'XYZ', count: 400
];
var ndx = crossfilter(data);
var dim = ndx.dimension(function(d)return [d.service, d.day];);
var grp = dim.group().reduceSum(function(d)  return d.count; );
grp = fillGroup(grp, d3.cross(['ABC', 'XYZ'], d3.range(1, 9)));

var chart= dc.seriesChart("#chart")
  .width(500)
  .height(180)
  .chart(function(c)  return dc.lineChart(c).renderArea(true).curve(d3.curveCardinal); )
  .dimension(dim)
  .group(grp)
  .brushOn(false)
  .seriesAccessor(function(d)  return d.key[0]; )
  .keyAccessor(function(d)  return d.key[1]; )
  .valueAccessor(function(d)  return +d.value; )
  .x(d3.scaleLinear())
  .elasticX(true)
  .y(d3.scaleLinear().domain([0, 450]))
  .legend(dc.legend().horizontal(false).x(60).y(10))
  .yAxisLabel("Count")
  .render();
    
function fillGroup(grupo, rango) 
  return 
    all:function () 
      var resultados = grupo.all().slice(0);
      var encontrado = ;
      resultados.forEach(function(d) 
        encontrado[d.key] = true;
      );
      rango.forEach(function(d) 
        if (!encontrado[d])  resultados.push(key: d, value: 0); 
      );
      return resultados;
    
  ;


/* Please ignore what follows - it's the minified SaveSvgAsPng library,
   I haven't found any CDN for it... */
(function()const out$=typeof exports!='undefined'&&exports||typeof define!='undefined'&&||this||window;if(typeof define!=='undefined')define(()=>out$);const xmlns='http://www.w3.org/2000/xmlns/';const doctype='<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [<!ENTITY nbsp "&#160;">]>';const urlRegex=/url\(["']?(.+?)["']?\)/;const fontFormats=woff2:'font/woff2',woff:'font/woff',otf:'application/x-font-opentype',ttf:'application/x-font-ttf',eot:'application/vnd.ms-fontobject',sfnt:'application/font-sfnt',svg:'image/svg+xml';const isElement=obj=>obj instanceof htmlElement||obj instanceof SVGElement;const requireDomNode=el=>if(!isElement(el))throw new Error(`an HTMLElement or SVGElement is required; got $el`);const isExternal=url=>url&&url.lastIndexOf('http',0)===0&&url.lastIndexOf(window.location.host)===-1;const getFontMimeTypeFromUrl=fontUrl=>const formats=Object.keys(fontFormats).filter(extension=>fontUrl.indexOf(`.$extension`)>0).map(extension=>fontFormats[extension]);if(formats)return formats[0];console.error(`Unknown font format for $fontUrl. Fonts may not be working correctly.`);return'application/octet-stream';const arrayBufferToBase64=buffer=>let binary='';const bytes=new Uint8Array(buffer);for(let i=0;i<bytes.byteLength;i++)binary+=String.fromCharCode(bytes[i]);return window.btoa(binary)
const getDimension=(el,clone,dim)=>const v=(el.viewBox&&el.viewBox.baseVal&&el.viewBox.baseVal[dim])||(clone.getAttribute(dim)!==null&&!clone.getAttribute(dim).match(/%$/)&&parseInt(clone.getAttribute(dim)))||el.getBoundingClientRect()[dim]||parseInt(clone.style[dim])||parseInt(window.getComputedStyle(el).getPropertyValue(dim));return typeof v==='undefined'||v===null||isNaN(parseFloat(v))?0:v;const getDimensions=(el,clone,width,height)=>if(el.tagName==='svg')returnwidth:width||getDimension(el,clone,'width'),height:height||getDimension(el,clone,'height');else if(el.getBBox)constx,y,width,height=el.getBBox();returnwidth:x+width,height:y+height;const reEncode=data=>decodeURIComponent(encodeURIComponent(data).replace(/%([0-9A-F]2)/g,(match,p1)=>const c=String.fromCharCode(`0x$p1`);return c==='%'?'%25':c));const uriToBlob=uri=>const byteString=window.atob(uri.split(',')[1]);const mimeString=uri.split(',')[0].split(':')[1].split(';')[0]
const buffer=new ArrayBuffer(byteString.length);const intArray=new Uint8Array(buffer);for(let i=0;i<byteString.length;i++)intArray[i]=byteString.charCodeAt(i)
return new Blob([buffer],type:mimeString);const query=(el,selector)=>if(!selector)return;tryreturn el.querySelector(selector)||el.parentNode&&el.parentNode.querySelector(selector)catch(err)console.warn(`Invalid CSS selector "$selector"`,err);const detectCssFont=rule=>const match=rule.cssText.match(urlRegex);const url=(match&&match[1])||'';if(!url||url.match(/^data:/)||url==='about:blank')return;const fullUrl=url.startsWith('../')?`$rule.href/../$url`:url.startsWith('./')?`$rule.href/.$url`:url;returntext:rule.cssText,format:getFontMimeTypeFromUrl(fullUrl),url:fullUrl;const inlineImages=el=>Promise.all(Array.from(el.querySelectorAll('image')).map(image=>let href=image.getAttributeNS('http://www.w3.org/1999/xlink','href')||image.getAttribute('href');if(!href)return Promise.resolve(null);if(isExternal(href))href+=(href.indexOf('?')===-1?'?':'&')+'t='+new Date().valueOf()
return new Promise((resolve,reject)=>const canvas=document.createElement('canvas');const img=new Image();img.crossOrigin='anonymous';img.src=href;img.onerror=()=>reject(new Error(`Could not load $href`));img.onload=()=>canvas.width=img.width;canvas.height=img.height;canvas.getContext('2d').drawImage(img,0,0);image.setAttributeNS('http://www.w3.org/1999/xlink','href',canvas.toDataURL('image/png'));resolve(!0))));const cachedFonts=;const inlineFonts=fonts=>Promise.all(fonts.map(font=>new Promise((resolve,reject)=>if(cachedFonts[font.url])return resolve(cachedFonts[font.url]);const req=new XMLHttpRequest();req.addEventListener('load',()=>const fontInBase64=arrayBufferToBase64(req.response);const fontUri=font.text.replace(urlRegex,`url("data:$font.format;base64,$fontInBase64")`)+'\n';cachedFonts[font.url]=fontUri;resolve(fontUri));req.addEventListener('error',e=>console.warn(`Failed to load font from: $font.url`,e);cachedFonts[font.url]=null;resolve(null));req.addEventListener('abort',e=>console.warn(`Aborted loading font from: $font.url`,e);resolve(null));req.open('GET',font.url);req.responseType='arraybuffer';req.send()))).then(fontCss=>fontCss.filter(x=>x).join(''));let cachedRules=null;const styleSheetRules=()=>if(cachedRules)return cachedRules;return cachedRules=Array.from(document.styleSheets).map(sheet=>tryreturn sheet.cssRulescatch(e)console.warn(`Stylesheet could not be loaded: $sheet.href`));const inlineCss=(el,options)=>constselectorRemap,modifyStyle,modifyCss,fonts=options||;const generateCss=modifyCss||((selector,properties)=>const sel=selectorRemap?selectorRemap(selector):selector;const props=modifyStyle?modifyStyle(properties):properties;return `$sel$props\n`);const css=[];const detectFonts=typeof fonts==='undefined';const fontList=fonts||[];styleSheetRules().forEach(rules=>if(!rules)return;Array.from(rules).forEach(rule=>if(typeof rule.style!='undefined')if(query(el,rule.selectorText))css.push(generateCss(rule.selectorText,rule.style.cssText));else if(detectFonts&&rule.cssText.match(/^@font-face/))const font=detectCssFont(rule);if(font)fontList.push(font)else css.push(rule.cssText)));return inlineFonts(fontList).then(fontCss=>css.join('\n')+fontCss);out$.prepareSvg=(el,options,done)=>requireDomNode(el);constleft=0,top=0,width:w,height:h,scale=1,responsive=!1,=options||;return inlineImages(el).then(()=>let clone=el.cloneNode(!0);constwidth,height=getDimensions(el,clone,w,h);if(el.tagName!=='svg')if(el.getBBox)clone.setAttribute('transform',clone.getAttribute('transform').replace(/translate\(.*?\)/,''));const svg=document.createElementNS('http://www.w3.org/2000/svg','svg');svg.appendChild(clone);clone=svgelseconsole.error('Attempted to render non-SVG element',el);return
clone.setAttribute('version','1.1');clone.setAttribute('viewBox',[left,top,width,height].join(' '));if(!clone.getAttribute('xmlns'))clone.setAttributeNS(xmlns,'xmlns','http://www.w3.org/2000/svg');if(!clone.getAttribute('xmlns:xlink'))clone.setAttributeNS(xmlns,'xmlns:xlink','http://www.w3.org/1999/xlink');if(responsive)clone.removeAttribute('width');clone.removeAttribute('height');clone.setAttribute('preserveAspectRatio','xMinYMin meet')elseclone.setAttribute('width',width*scale);clone.setAttribute('height',height*scale)
Array.from(clone.querySelectorAll('foreignObject > *')).forEach(foreignObject=>if(!foreignObject.getAttribute('xmlns'))
foreignObject.setAttributeNS(xmlns,'xmlns','http://www.w3.org/1999/xhtml'));return inlineCss(el,options).then(css=>const style=document.createElement('style');style.setAttribute('type','text/css');style.innerHTML=`<![CDATA[\n$css\n]]>`;const defs=document.createElement('defs');defs.appendChild(style);clone.insertBefore(defs,clone.firstChild);const outer=document.createElement('div');outer.appendChild(clone);const src=outer.innerHTML.replace(/NS\d+:href/gi,'xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href');if(typeof done==='function')done(src,width,height);else returnsrc,width,height));out$.svgAsDataUri=(el,options,done)=>requireDomNode(el);const result=out$.prepareSvg(el,options).then((src)=>`data:image/svg+xml;base64,$window.btoa(reEncode(doctype+src))`);if(typeof done==='function')return result.then(done);return result;out$.svgAsPngUri=(el,options,done)=>requireDomNode(el);constencoderType='image/png',encoderOptions=0.8,backgroundColor,canvg=options||;const convertToPng=(src,width,height)=>const canvas=document.createElement('canvas');const context=canvas.getContext('2d');const pixelRatio=window.devicePixelRatio||1;canvas.width=width*pixelRatio;canvas.height=height*pixelRatio;canvas.style.width=`$canvas.widthpx`;canvas.style.height=`$canvas.heightpx`;context.setTransform(pixelRatio,0,0,pixelRatio,0,0);if(canvg)canvg(canvas,src);else context.drawImage(src,0,0);if(backgroundColor)context.globalCompositeOperation='destination-over';context.fillStyle=backgroundColor;context.fillRect(0,0,canvas.width,canvas.height)
let png;trypng=canvas.toDataURL(encoderType,encoderOptions)catch(e)if((typeof SecurityError!=='undefined'&&e instanceof SecurityError)||e.name==='SecurityError')console.error('Rendered SVG images cannot be downloaded in this browser.');returnelse throw e
if(typeof done==='function')done(png);return Promise.resolve(png)
if(canvg)return out$.prepareSvg(el,options).then(convertToPng);else return out$.svgAsDataUri(el,options).then(uri=>return new Promise((resolve,reject)=>const image=new Image();image.onload=()=>resolve(convertToPng(src:image,width:image.width,height:image.height));image.onerror=()=>reject(`There was an error loading the data URI as an image on the following SVG\n$window.atob(uri.slice(26))Open the following link to see browser's diagnosis\n$uri`)
image.src=uri));out$.download=(name,uri)=>if(navigator.msSaveOrOpenBlob)navigator.msSaveOrOpenBlob(uriToBlob(uri),name);elseconst saveLink=document.createElement('a');if('download' in saveLink)saveLink.download=name;saveLink.style.display='none';document.body.appendChild(saveLink);tryconst blob=uriToBlob(uri);const url=URL.createObjectURL(blob);saveLink.href=url;saveLink.onclick=()=>requestAnimationFrame(()=>URL.revokeObjectURL(url))catch(e)console.warn('This browser does not support object URLs. Falling back to string URL.');saveLink.href=uri
saveLink.click();document.body.removeChild(saveLink)
elsewindow.open(uri,'_temp','menubar=no,toolbar=no,status=no');out$.saveSvg=(el,name,options)=>requireDomNode(el);out$.svgAsDataUri(el,options||,uri=>out$.download(name,uri));out$.saveSvgAsPng=(el,name,options)=>requireDomNode(el);out$.svgAsPngUri(el,options||,uri=>out$.download(name,uri)))()
circle.dot  fill-opacity:0.5 !important; 

/* Please ignore what follows - it's the minified version of
   https://cdnjs.cloudflare.com/ajax/libs/dc/3.0.4/dc.css, I had to include it here
   because if it's stored in a different domain, SaveSvgAsPng can't load it */
.dc-chart path.dc-symbol,.dc-legend g.dc-legend-item.fadeoutfill-opacity:.5;stroke-opacity:.5div.dc-chartfloat:left.dc-chart rect.barstroke:none;cursor:pointer.dc-chart rect.bar:hoverfill-opacity:.5.dc-chart rect.deselectedstroke:none;fill:#ccc.dc-chart .pie-slicefill:#fff;font-size:12px;cursor:pointer.dc-chart .pie-slice.externalfill:#000.dc-chart .pie-slice :hover,.dc-chart .pie-slice.highlightfill-opacity:.8.dc-chart .pie-pathfill:none;stroke-width:2px;stroke:#000;opacity:.4.dc-chart .selected path,.dc-chart .selected circlestroke-width:3;stroke:#ccc;fill-opacity:1.dc-chart .deselected path,.dc-chart .deselected circlestroke:none;fill-opacity:.5;fill:#ccc.dc-chart .axis path,.dc-chart .axis linefill:none;stroke:#000;shape-rendering:crispEdges.dc-chart .axis textfont:10px sans-serif.dc-chart .grid-line,.dc-chart .axis .grid-line,.dc-chart .grid-line line,.dc-chart .axis .grid-line linefill:none;stroke:#ccc;opacity:.5;shape-rendering:crispEdges.dc-chart .brush rect.selectionfill:#4682b4;fill-opacity:.125.dc-chart .brush .custom-brush-handlefill:#eee;stroke:#666;cursor:ew-resize.dc-chart path.linefill:none;stroke-width:1.5px.dc-chart path.areafill-opacity:.3;stroke:none.dc-chart path.highlightstroke-width:3;fill-opacity:1;stroke-opacity:1.dc-chart g.statecursor:pointer.dc-chart g.state :hoverfill-opacity:.8.dc-chart g.state pathstroke:#fff.dc-chart g.deselected pathfill:gray.dc-chart g.deselected textdisplay:none.dc-chart g.row rectfill-opacity:.8;cursor:pointer.dc-chart g.row rect:hoverfill-opacity:.6.dc-chart g.row textfill:#fff;font-size:12px;cursor:pointer.dc-chart g.dc-tooltip pathfill:none;stroke:gray;stroke-opacity:.8.dc-chart g.county pathstroke:#fff;fill:none.dc-chart g.debug rectfill:#00f;fill-opacity:.2.dc-chart g.axis text-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;pointer-events:none.dc-chart .nodefont-size:.7em;cursor:pointer.dc-chart .node :hoverfill-opacity:.8.dc-chart .bubblestroke:none;fill-opacity:.6.dc-chart .highlightfill-opacity:1;stroke-opacity:1.dc-chart .fadeoutfill-opacity:.2;stroke-opacity:.2.dc-chart .box textfont:10px sans-serif;-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;pointer-events:none.dc-chart .box linefill:#fff.dc-chart .box rect,.dc-chart .box line,.dc-chart .box circlestroke:#000;stroke-width:1.5px.dc-chart .box .centerstroke-dasharray:3,3.dc-chart .box .datastroke:none;stroke-width:0.dc-chart .box .outlierfill:none;stroke:#ccc.dc-chart .box .outlierBoldfill:red;stroke:none.dc-chart .box.deselectedopacity:.5.dc-chart .box.deselected .boxfill:#ccc.dc-chart .symbolstroke:none.dc-chart .heatmap .box-group.deselected rectstroke:none;fill-opacity:.5;fill:#ccc.dc-chart .heatmap g.axis textpointer-events:all;cursor:pointer.dc-chart .empty-chart .pie-slicecursor:default.dc-chart .empty-chart .pie-slice pathfill:#fee;cursor:default.dc-chart circle.dotstroke:none.dc-data-countfloat:right;margin-top:15px;margin-right:15px.dc-data-count .filter-count,.dc-data-count .total-countcolor:#3182bd;font-weight:700.dc-legendfont-size:11px.dc-legend .dc-legend-itemcursor:pointer.dc-hard .number-displayfloat:nonediv.dc-html-legendoverflow-y:auto;overflow-x:hidden;height:inherit;float:right;padding-right:2pxdiv.dc-html-legend .dc-legend-item-horizontaldisplay:inline-block;margin-left:5px;margin-right:5px;cursor:pointerdiv.dc-html-legend .dc-legend-item-horizontal.selectedbackground-color:#3182bd;color:whitediv.dc-html-legend .dc-legend-item-verticaldisplay:block;margin-top:5px;padding-top:1px;padding-bottom:1px;cursor:pointerdiv.dc-html-legend .dc-legend-item-vertical.selectedbackground-color:#3182bd;color:whitediv.dc-html-legend .dc-legend-item-colordisplay:table-cell;width:12px;height:12pxdiv.dc-html-legend .dc-legend-item-labelline-height:12px;display:table-cell;vertical-align:middle;padding-left:3px;padding-right:3px;font-size:.75em.dc-html-legend-containerheight:inherit
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/crossfilter/1.3.12/crossfilter.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/dc/3.0.4/dc.min.js"></script>
<div id="chart"></div>
<button id="export" onclick="save()">Export as PNG</button>

基本上,我只是获取 SVG DOM 元素,并将其传递给 saveSvgAsPng 函数:

var chart = document.getElementById('chart').getElementsByTagName('svg')[0];
saveSvgAsPng(chart, 'chart.png', options);

这是 dc.js 图表的样子:

这就是导出的 PNG 的样子:

为什么它会在 X 轴下方显示线/区域/圆(也超出水平限制)?我该如何解决?

&lt;defs&gt;&lt;clipPath /&gt;&lt;/defs&gt; 部分存在于 SVG 元素中,我猜它的定义正确(对吗?)。

【问题讨论】:

看起来 clipPath 不见了 - 我没有发现有人抱怨此功能在 saveSvgAsPng 中不起作用,所以我的猜测是,由于 clip-path attribute 使用 URL,链接以某种方式断开。注意 dc.js uses an absolute URL here for compatibility reasons. 感谢 Gordon,这实际上是问题所在。我想知道为什么会这样。无论如何...解决方法很容易:) 啊,现在我明白了。我有相反的问题:我希望在 clipPath 之外绘制气泡图的气泡;) @Xavier,您可以使用clipPadding 放大剪辑框,也可以使用chart.select('g.chart-body').attr('clip-path', null) 将其完全删除。 【参考方案1】:

我没有尝试过 saveSvgAsPng,所以这只是一个猜测,但你可以尝试

chart.select('g.chart-body').attr('clip-path',
     chart.select('g.chart-body').attr('clip-path').replace(/.*#/, 'url(#'))

推理:dc.js 使用带有绝对 URL 的 clip-path attribute 的模糊形式。它正在使用window.location.href 查找当前页面的 URL,这可能会出错,或者 saveSvgAsPng 可能不需要绝对 URL。

It does this for Angular compatibility 但我明白为什么这会使图书馆感到困惑。

上面的代码将删除基本 URL,只留下相对哈希部分。

如果这有帮助,我们可以为此行为添加一个选项。

【讨论】:

再次感谢 Gordon,解决了它(好吧,我实际上选择了 #chart svg *[clip-path] 并将 http.*# 替换为 #,但它是一样的)。 我不确定是库引起了混淆,还是我的 node.js 应用程序配置。如果绝对 URL 强制向服务器发出新请求,则问题可能是由于我在路由文件中做错了什么引起的。无论如何,如果有一个功能可以直接在 dc.js 中更改行为,那就太好了……除非我是唯一一个对绝对 URL 有问题的人 :)【参考方案2】:

我不是自行回答,我只是想添加一个旁注,这可能对其他 SaveSvgAsPng 用户有所帮助:

为了使导出的 PNG 具有与 SVG 相同的外观,SaveSvgAsPng 需要正确应用 CSS 样式。否则,它看起来像这样:

如果你遇到这个问题,请注意:

    样式表需要与 javascript 代码存储在同一个域中,否则库将无法加载它们(出于安全原因)。

    大多数 dc.js 的样式都应用于 .dc-chart 类或其子类。这个 CSS 类应用于父 DIV,而不是应用于 SaveSvgAsPng 导出的 SVG 元素。因此,您必须从 CSS 规则中删除选择器。最简单的方法是使用selectorRemap 选项,如下所示:

var options = 
  selectorRemap: function(s)  return s.replace(/\.dc-chart/g, ''); 
;

var chart = document.getElementById('chart').getElementsByTagName('svg')[0];
saveSvgAsPng(chart, 'chart.png', options);

【讨论】:

【参考方案3】:

我对 saveSvgAsPng 不熟悉,可能它已经在使用画布了。如果是这种情况,请否决我的问题,可能不会有用;)

您是否尝试过使用 svg->canvas->png 路径?我确实将它与其他 d3 项目一起使用并且工作正常。

这是从该问题的另一个答案中提取的 sn-p:

var btn = document.querySelector('button');
var svg = document.querySelector('svg');
var canvas = document.querySelector('canvas');

function triggerDownload (imgURI) 
  var evt = new MouseEvent('click', 
    view: window,
    bubbles: false,
    cancelable: true
  );

  var a = document.createElement('a');
  a.setAttribute('download', 'MY_COOL_IMAGE.png');
  a.setAttribute('href', imgURI);
  a.setAttribute('target', '_blank');

  a.dispatchEvent(evt);


btn.addEventListener('click', function () 
  var canvas = document.getElementById('canvas');
  var ctx = canvas.getContext('2d');
  var data = (new XMLSerializer()).serializeToString(svg);
  var DOMURL = window.URL || window.webkitURL || window;

  var img = new Image();
  var svgBlob = new Blob([data], type: 'image/svg+xml;charset=utf-8');
  var url = DOMURL.createObjectURL(svgBlob);

  img.onload = function () 
    ctx.drawImage(img, 0, 0);
    DOMURL.revokeObjectURL(url);

    var imgURI = canvas
        .toDataURL('image/png')
        .replace('image/png', 'image/octet-stream');

    triggerDownload(imgURI);
  ;

  img.src = url;
);
<button>svg to png</button>

<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1"  >
  <rect x="10" y="10"   />
  <text x="0" y="100">Look, i'm cool</text>
</svg>

<canvas id="canvas"></canvas>

【讨论】:

谢谢泽维尔。我相信这与 SaveSvgAsPng 为我所做的相同,或者非常相似,至少画布、'2d' 和 drawImage 也都在那里。如果我不能让它工作,我想我可以修改这个代码并摆脱这个库。 -- (另外......为什么有人会反对试图帮助他们的人?:) 您不会对试图提供帮助的人投反对票,而是对错误答案投反对票;)

以上是关于将 dc.js 图表从 SVG 导出到 PNG的主要内容,如果未能解决你的问题,请参考以下文章

Highcharts 导出功能

从 R 中的 Plotly 导出 PNG 文件

SVG2PNG(前台个后台将SVG转换为PNG,完美支持IE8下载)--amcharts导出png

如何使用 canvg.js 和 Canvas 将 SVG 导出为 PNG

将画布从 React Konva 导出为 SVG 和 PDF?

如何将 angularjs-nvd3 图表导出到文件