svg - 将 dc.js 图表从 SVG 导出为 PNG

标签 svg dc.js clip

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

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')return{width:width||getDimension(el,clone,'width'),height:height||getDimension(el,clone,'height')};else if(el.getBBox){const{x,y,width,height}=el.getBBox();return{width: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;try{return 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;return{text: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=>{try{return sheet.cssRules}catch(e){console.warn(`Stylesheet could not be loaded: ${sheet.href}`)}})};const inlineCss=(el,options)=>{const{selectorRemap,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);const{left=0,top=0,width:w,height:h,scale=1,responsive=!1,}=options||{};return inlineImages(el).then(()=>{let clone=el.cloneNode(!0);const{width,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=svg}else{console.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')}else{clone.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 return{src,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);const{encoderType='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.width}px`;canvas.style.height=`${canvas.height}px`;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;try{png=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.');return}else 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);else{const saveLink=document.createElement('a');if('download' in saveLink){saveLink.download=name;saveLink.style.display='none';document.body.appendChild(saveLink);try{const 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)}
else{window.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.fadeout{fill-opacity:.5;stroke-opacity:.5}div.dc-chart{float:left}.dc-chart rect.bar{stroke:none;cursor:pointer}.dc-chart rect.bar:hover{fill-opacity:.5}.dc-chart rect.deselected{stroke:none;fill:#ccc}.dc-chart .pie-slice{fill:#fff;font-size:12px;cursor:pointer}.dc-chart .pie-slice.external{fill:#000}.dc-chart .pie-slice :hover,.dc-chart .pie-slice.highlight{fill-opacity:.8}.dc-chart .pie-path{fill:none;stroke-width:2px;stroke:#000;opacity:.4}.dc-chart .selected path,.dc-chart .selected circle{stroke-width:3;stroke:#ccc;fill-opacity:1}.dc-chart .deselected path,.dc-chart .deselected circle{stroke:none;fill-opacity:.5;fill:#ccc}.dc-chart .axis path,.dc-chart .axis line{fill:none;stroke:#000;shape-rendering:crispEdges}.dc-chart .axis text{font:10px sans-serif}.dc-chart .grid-line,.dc-chart .axis .grid-line,.dc-chart .grid-line line,.dc-chart .axis .grid-line line{fill:none;stroke:#ccc;opacity:.5;shape-rendering:crispEdges}.dc-chart .brush rect.selection{fill:#4682b4;fill-opacity:.125}.dc-chart .brush .custom-brush-handle{fill:#eee;stroke:#666;cursor:ew-resize}.dc-chart path.line{fill:none;stroke-width:1.5px}.dc-chart path.area{fill-opacity:.3;stroke:none}.dc-chart path.highlight{stroke-width:3;fill-opacity:1;stroke-opacity:1}.dc-chart g.state{cursor:pointer}.dc-chart g.state :hover{fill-opacity:.8}.dc-chart g.state path{stroke:#fff}.dc-chart g.deselected path{fill:gray}.dc-chart g.deselected text{display:none}.dc-chart g.row rect{fill-opacity:.8;cursor:pointer}.dc-chart g.row rect:hover{fill-opacity:.6}.dc-chart g.row text{fill:#fff;font-size:12px;cursor:pointer}.dc-chart g.dc-tooltip path{fill:none;stroke:gray;stroke-opacity:.8}.dc-chart g.county path{stroke:#fff;fill:none}.dc-chart g.debug rect{fill:#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 .node{font-size:.7em;cursor:pointer}.dc-chart .node :hover{fill-opacity:.8}.dc-chart .bubble{stroke:none;fill-opacity:.6}.dc-chart .highlight{fill-opacity:1;stroke-opacity:1}.dc-chart .fadeout{fill-opacity:.2;stroke-opacity:.2}.dc-chart .box text{font: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 line{fill:#fff}.dc-chart .box rect,.dc-chart .box line,.dc-chart .box circle{stroke:#000;stroke-width:1.5px}.dc-chart .box .center{stroke-dasharray:3,3}.dc-chart .box .data{stroke:none;stroke-width:0}.dc-chart .box .outlier{fill:none;stroke:#ccc}.dc-chart .box .outlierBold{fill:red;stroke:none}.dc-chart .box.deselected{opacity:.5}.dc-chart .box.deselected .box{fill:#ccc}.dc-chart .symbol{stroke:none}.dc-chart .heatmap .box-group.deselected rect{stroke:none;fill-opacity:.5;fill:#ccc}.dc-chart .heatmap g.axis text{pointer-events:all;cursor:pointer}.dc-chart .empty-chart .pie-slice{cursor:default}.dc-chart .empty-chart .pie-slice path{fill:#fee;cursor:default}.dc-chart circle.dot{stroke:none}.dc-data-count{float:right;margin-top:15px;margin-right:15px}.dc-data-count .filter-count,.dc-data-count .total-count{color:#3182bd;font-weight:700}.dc-legend{font-size:11px}.dc-legend .dc-legend-item{cursor:pointer}.dc-hard .number-display{float:none}div.dc-html-legend{overflow-y:auto;overflow-x:hidden;height:inherit;float:right;padding-right:2px}div.dc-html-legend .dc-legend-item-horizontal{display:inline-block;margin-left:5px;margin-right:5px;cursor:pointer}div.dc-html-legend .dc-legend-item-horizontal.selected{background-color:#3182bd;color:white}div.dc-html-legend .dc-legend-item-vertical{display:block;margin-top:5px;padding-top:1px;padding-bottom:1px;cursor:pointer}div.dc-html-legend .dc-legend-item-vertical.selected{background-color:#3182bd;color:white}div.dc-html-legend .dc-legend-item-color{display:table-cell;width:12px;height:12px}div.dc-html-legend .dc-legend-item-label{line-height:12px;display:table-cell;vertical-align:middle;padding-left:3px;padding-right:3px;font-size:.75em}.dc-html-legend-container{height: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 图表的样子:

expected result

这就是导出的 PNG 的样子:

actual result

为什么它在 X 轴下方显示线条/区域/圆(并且也超出水平限制)?我该如何修复它?

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

最佳答案

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

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

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

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

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

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

关于svg - 将 dc.js 图表从 SVG 导出为 PNG,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/51281972/

相关文章:

ios - 没有出现在 iphone 和 firefox 上的 webfont

javascript - 我可以让 elasticX()、elasticY() 仅适用于上限、dc.js 吗?

javascript - dc js - 使用 dc js 的直方图

css - 使用 SVG 路径剪辑 div

java - 尝试在 JFrame 上播放声音时出现 AudioClip 问题

javascript - 为什么 getAttributeNS 返回 null?

javascript - 如何使用 javascript 将工具提示添加到 svg 图形元素

css - SVG:viewBox 中宽度非常小的文本(1 个单位宽)

d3.js - 是否可以有一个滚动 div 来调整 dc.js 行图中的条数

css - 如何拥有多个 css 裁剪 Sprite ?