html2canvas does not print jsPlumb Connectors - svg

How to print SVG elements that are built by jsPlumb.
Known that getting all SVG Elements drawen by jsPlumb is retrieved by this code :
var uiJsPlumbConnectors=jsPlumb.getAllConnections().map(function(conn){return conn.canvas;})
All connectors are SVG elements :
Using html2canvas to print all connectors (SVG), it does not work :
html2canvas(uiJsPlumbConnectors).then(function(c){
window.open(c.toDataURL('image/png'))
});
An image has been generated , however, it is an emply image .
FIDDLE
It seems that html2canvas does not support yet multi-elements drawing ?

Last time I checked html2canvas was not able to convert SVGs, you will need another script to handle that.
The steps:
transfer html elements to canvas
transfer svg elements to canvas
export canvas
I used https://code.google.com/p/canvg/ to export to the same canvas after using html2canvas. Hope that helps you.

I just implemented this
<%--stuff for printing--%>
<script type="text/javascript" src="../../../../Scripts/Print/html2canvas.js"></script>
<script src="<%=AdminPath%>Scripts/canvg/rgbcolor.js" type="text/javascript"></script>
<script src="<%=AdminPath%>Scripts/canvg/StackBlur.js" type="text/javascript"></script>
<script src="<%=AdminPath%>Scripts/canvg/canvg.js" type="text/javascript"></script>
jsplumb div
<div class="demo statemachine-demo" id="statemachine-demo" style="margin: 0px;">
</div>
hidden div for printing
<div id="canvasDiv" style='visibility:hidden;' >
</div>
function renderImage()
{
var statemachinediv = document.getElementById('statemachine-demo');
html2canvas([statemachinediv], {
onrendered: function (canvas) {
document.getElementById('canvasDiv').appendChild(canvas);
var svgList = $(statemachinediv).find( "svg" );
svgList.each(function(index, value) {
try
{
var svgExample = this;
var serializer = new XMLSerializer();
var svgMarkup = serializer.serializeToString(svgExample);
if(svgMarkup.indexOf("_jsPlumb_connector") > -1)
{
var leftIndex = svgMarkup.indexOf("left: ");
var endOfLeft = svgMarkup.indexOf("px", leftIndex);
var leftPosition = svgMarkup.substring(leftIndex+6, endOfLeft );
var left = parseInt(leftPosition);
var topIndex = svgMarkup.indexOf("top: ");
var endOfTop = svgMarkup.indexOf("px", topIndex);
var topPosition = svgMarkup.substring(topIndex+5, endOfTop );
var top = parseInt(topPosition);
svgMarkup = svgMarkup.replace('xmlns="http://www.w3.org/2000/svg"','');
var connectorCanvas = document.createElement('canvas');
canvg(connectorCanvas, svgMarkup); //add connector to canvas
var context = canvas.getContext('2d');
context.drawImage(connectorCanvas, left, top);
}
}
catch(err)
{
showBalloon('error in print');
}
});
var stateMachineName = $("#stateMachineDropDown option:selected").text();
var data = canvas.toDataURL('image/png');
var mywindow = window.open('', 'my div', 'height=400,width=600');
mywindow.document.write('<html><head><title>' + stateMachineName + '</title>');
mywindow.document.write('</head><body ><table><tr><td>');
mywindow.document.write('</td></tr></table><img src="' + data + '" />');
mywindow.document.write('</body></html>');
mywindow.print();
}
});
return false;
}

Its a old question, but, this helped me. Mode details.
$clone.find('.jtk-connector').each(function () {
// for every SVG element created by JsPlumb for connections...
var left = parseInt(this.style.left, 10) + 'px';
var top = parseInt(this.style.top, 10) + 'px';
this.removeAttribute('style');
this.removeAttribute('position');
this.setAttribute('width', parseInt(this.getAttribute('width'), 10) + 'px');
this.setAttribute('height', parseInt(this.getAttribute('height'), 10) + 'px');
this.setAttribute('preserveAspectRatio', 'xMidYMid meet');
this.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
// this.children[0] is the path for connection line
// this.children[1] is the path for connection arrow shape
this.children[0].setAttribute('xmlns', 'http://www.w3.org/2000/svg');
this.children[1].setAttribute('xmlns', 'http://www.w3.org/2000/svg');
this.setAttribute('viewbox', '0 0 ' + parseInt(this.getAttribute('width'), 10) + ' ' + parseInt(this.getAttribute('height'), 10));
this.children[0].setAttribute('stroke-width', '2px');
this.children[0].setAttribute('stroke', '#c9c9c9');
this.children[1].setAttribute('fill', '#c9c9c9');
this.children[1].setAttribute('stroke', '#c9c9c9');
$clone.find(this).wrap('<span style="position: absolute; left: ' + left + '; top: ' + top + ';"></span>');
});

Related

get data from nodejs to frontend js for google maps but stuck in retrieving it looking for ways to solve it

My NodeJS GET route:
router.get('/stores', function (req, res, next) {
errorMsg = req.flash('error')[0];
successMsg = req.flash('success')[0];
Product.find(function (err, products) {
// console.log(products)
res.render('admin/stores', {
layout: 'admin-map.hbs',
stores: products,
errorMsg: errorMsg,
GOOGLE_APIKEY: process.env.GOOGLE_APIKEY,
noErrors: 1
});
});
The route /stores returns json data which holds latitude and longitude and I want it in my script tag of map.html with loop, to render the pins on the map. Below, the script:
<!DOCTYPE html>
<html>
<head>
<script src = "https://maps.googleapis.com/maps/api/js"></script>
<script>
function loadMap() {
alert(this.stores);
var mapOptions = {
center:new google.maps.LatLng(17.433053, 78.412172),
zoom:5
}
var map = new google.maps.Map(document.getElementById("sample"),mapOptions);
var marker = new google.maps.Marker({
position: new google.maps.LatLng(17.433053, 78.412172),
map: map,
draggable:true,
icon:'/scripts/img/logo-footer.png'
});
marker.setMap(map);
var infowindow = new google.maps.InfoWindow({ });
infowindow.open(map,marker);
}
</script>
<!-- ... -->
</head>
</html>
How can I do it?
It seems you need to follow two steps
1. Pass data from hbs to script
Using triple brackets syntax
<script>
let stores = {{{ stores }}}; // the triple brackets
console.log('Data : ', stores);
function loadMap() {
...
Check if data is being printed in console? If yes your data is available in the front-end script and you can
2. Loop through it
...
for (let i = 0; i < stores.length; i++) {
// the JS loop instead of hbs one, because we are on front-end
var marker = new google.maps.Marker({
position: new google.maps.LatLng(stores[i].lat, stores[i].lng), // whatever applies
map: map,
draggable:true,
icon:'/scripts/img/logo-footer.png'
});
}
And donot need to call setMap(), you have already set the map in map: map above
the answer is inside fronted script i can have an object declared globally and this double flower brackets works fine with them
<script>
function loadMap() {
alert(this.stores);
var mapOptions = {
center:new google.maps.LatLng(17.433053, 78.412172),
zoom:5
}
var map = new google.maps.Map(document.getElementById("sample"),mapOptions);
{{#each stores}}
var marker = new google.maps.Marker({
position: new google.maps.LatLng(17.433053, 78.412172),
map: map,
draggable:true,
icon:'/scripts/img/logo-footer.png'
});
{{/each}}
marker.setMap(map);
var infowindow = new google.maps.InfoWindow({ });
infowindow.open(map,marker);
}
</script>

Exporting dc.js chart from SVG to PNG

I have a dc.js chart and I want to export it as a PNG image, using 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 " ">]>';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>
Basically, I just get the SVG DOM element, and pass it to the saveSvgAsPng function:
var chart = document.getElementById('chart').getElementsByTagName('svg')[0];
saveSvgAsPng(chart, 'chart.png', options);
This is how the dc.js chart looks like:
And this is how the exported PNG looks:
Why does it show lines/areas/circles under the X axis (and beyond the horizontal limits too)? How can I fix it?
The <defs><clipPath /></defs> section is present within the SVG element, and I guess it's properly defined (right?).
I haven't tried saveSvgAsPng, so this is just a guess, but you could try
chart.select('g.chart-body').attr('clip-path',
chart.select('g.chart-body').attr('clip-path').replace(/.*#/, 'url(#'))
Reasoning: dc.js uses an obscure form of the clip-path attribute with an absolute URL. It's looking for the URL of the current page using window.location.href and that could go wrong, or saveSvgAsPng might not expect an absolute URL.
It does this for Angular compatibility but I can see why this would confuse a library.
The code above will remove the base URL, leaving only the relative hash part.
If this helps, we can add an option for this behavior.
I'm not self-answering, I just want to add a side note, which might be helpful for other SaveSvgAsPng users:
For the exported PNG to have the same look as the SVG, SaveSvgAsPng needs to properly apply the CSS styles. Otherwise, it would look like this:
If you run into this problem, please note that:
The stylesheets need to be stored in the same domain as the javascript code, otherwise the library won't be able to load them (for security reasons).
Most dc.js' styles are applied to the .dc-chart class or its children. This CSS class is applied to the parent DIV, not to the SVG element, which is what SaveSvgAsPng exports. Therefore, you will have to remove the selector from the CSS rules. The easiest way to do so is using the selectorRemap option, like this:
var options = {
selectorRemap: function(s) { return s.replace(/\.dc-chart/g, ''); }
};
var chart = document.getElementById('chart').getElementsByTagName('svg')[0];
saveSvgAsPng(chart, 'chart.png', options);
I'm not familiar with saveSvgAsPng, it might be that it's already using canvas. If It's the case, please downvote my question, probably not going to be useful ;)
Did you try using the svg->canvas->png path? I did use it with other d3 projects and worked fine.
This is a snippet lifted from another answer on that question:
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" width="200" height="200">
<rect x="10" y="10" width="50" height="50" />
<text x="0" y="100">Look, i'm cool</text>
</svg>
<canvas id="canvas"></canvas>

signature-pad - resize not working

i'm using the signature-pad plugin and i'm having some issues whith the resize event:
- Multiple resizes lead to a loss in quality and the signature "moves" at each resize of the browser window ending with no signature in canvas.
- In some cases, the isEmpty() function wont work and i'll be able to save the empty signature.
Optional question : how can i detect an empty signature on php side ?
Thank you :)
Below my code :
$(window).resize(function() {
resizeCanvas();
});
var wrapper1 = document.getElementById("signature-pad"),
clearButton1 = wrapper1.querySelector("[data-action=clear]"),
canvas1 = wrapper1.querySelector("canvas"),
signaturePad1;
var wrapper2 = document.getElementById("signature-pad-paraphe"),
clearButton2 = wrapper2.querySelector("[data-action=clear]"),
canvas2 = wrapper2.querySelector("canvas"),
signaturePad2;
// Adjust canvas coordinate space taking into account pixel ratio,
// to make it look crisp on mobile devices.
// This also causes canvas to be cleared.
signaturePad1 = new SignaturePad(canvas1);
signaturePad2 = new SignaturePad(canvas2);
function resizeCanvas() {
//Sauvegarde sig / par
var sig = signaturePad1.toDataURL();
var par = signaturePad2.toDataURL();
var ratio = Math.max(window.devicePixelRatio || 1, 1);
canvas1.width = canvas1.offsetWidth * ratio;
canvas1.height = canvas1.offsetHeight * ratio;
canvas1.getContext("2d").scale(ratio, ratio);
canvas2.width = canvas2.offsetWidth * ratio;
canvas2.height = canvas2.offsetHeight * ratio;
canvas2.getContext("2d").scale(ratio, ratio);
// redraw
signaturePad1.fromDataURL(sig);
signaturePad2.fromDataURL(par);
}
window.onresize = resizeCanvas;
resizeCanvas();
// Init -> retourne la bonne valeur de isEmpty -> !!? Not sure if needed
signaturePad1.clear();
signaturePad2.clear();
var signature = $('#confirm_delete_signature').val();
if(signature){
signaturePad1.fromDataURL(signature);
}
var paraphe = $('#confirm_delete_paraphe').val();
if(paraphe){
signaturePad2.fromDataURL(paraphe);
}
clearButton1.addEventListener("click", function (event) {
signaturePad1.clear();
});
clearButton2.addEventListener("click", function (event) {
signaturePad2.clear();
});
Here is i developed a little solution;
Here are two key DOM elements:
div#id_wrapper
canvas#id
Considered it may be applied at devices with different devicePixelRatio and on screens changins theirs width (f.i.: portrait-landscape orientation).
export class FlexSignatureComponent extends React.Component {
state = {
width: 0,
lines: [],
storedValue: undefined,
validationClass: '', // toggles between 'is-invalid'/'is-valid'
validationMessage: ''
}
The lib initiation is right after the component got loaded:
componentDidMount = () => {
this.signPad = new SignaturePad(document.getElementById(this.props.htmlid), {
onEnd: this.onChangeSignaturePad,
backgroundColor: '#fff'
});
if (this.valueHolder.current.value) {
const data = JSON.parse(this.valueHolder.current.value);
this.state.lines = data.value;
this.state.width = 100;
}
//you need the next workarounds if you have other onWidnowResize handlers manarging screen width
//setTimeout-0 workaround to move windowResizeHandling at the end of v8-enging procedures queue
// otherwise omit setTimeout and envoke func as it is
setTimeout(this.handleWindowResize, 0);
window.addEventListener("resize", () => setTimeout(this.handleWindowResize, 0));
}
First handle window resize change
handleWindowResize = () => {
if (this.state.storedValue) {
const prevWrapperWidth = this.state.width;
const currentWrapperWidth = $(`#${this.props.htmlid}_wrapper`).width();
const scale = prevWrapperWidth / currentWrapperWidth;
this.state.width = currentWrapperWidth;
this.setRescaledSignature(this.state.lines, scale);
this.resetCanvasSize();
this.signPad.fromData(this.state.lines)
} else
this.resetCanvasSize()
}
Second rescaleSignature to another width
setRescaledSignature = (lines, scale) => {
lines.forEach(line => {
line.points.forEach(point => {
point.x /= scale;
point.y /= scale;
});
});
}
Finally updated canvas size
resetCanvasSize = () => {
const canvas = document.getElementById(this.props.htmlid);
canvas.style.width = '100%';
canvas.style.height = canvas.offsetWidth / 1.75 + "px";
canvas.width = canvas.offsetWidth * devicePixelRatio;
canvas.height = canvas.offsetHeight * devicePixelRatio;
canvas.getContext("2d").scale(devicePixelRatio, devicePixelRatio);
}
Here we on every change add new drawn line to this.state.lines
and prepare the lines to be submited as json.
But before the submission they need to create deepCopy and to be rescaled to conventional size (its width is equal 100px and DPR is 1)
onChangeSignaturePad = () => {
const value = this.signPad.toData();
this.state.lines = value;
const currentWrapperWidth = $(`#${this.props.htmlid}_wrapper`).width();
const scale = currentWrapperWidth / 100;
const ratio = 1 / devicePixelRatio;
const linesCopy = JSON.parse(JSON.stringify(value));
this.setRescaledSignature(linesCopy, scale, ratio);
const data = {
signature_configs: {
devicePixelRatio: 1,
wrapper_width: 100
},
value: linesCopy
};
this.state.storedValue = JSON.stringify(data);
this.validate()
}
One more thing is the red button to swipe the previous signatures
onClickClear = (e) => {
e.stopPropagation();
this.signPad.clear();
this.valueHolder.current.value = null;
this.validate()
}
render() {
let {label, htmlid} = this.props;
const {validationClass = ''} = this.state;
return (
<div className="form-group fs_form-signature">
<label>{Label}</label>
<div className="fs_wr-signature">
<button className={'fs_btn-clear'} onClick={this.onClickClear}>
<i className="fas fa-times"></i>
</button>
<div id={htmlid + '_wrapper'} className={`w-100 fs_form-control ${validationClass}`}>
<canvas id={htmlid}/>
</div>
</div>
<div className={' invalid-feedback fs_show-feedback ' + validationClass}>Signature is a mandatory field</div>
</div>
)
}
postWillUnmount() {
this.signPad.off();
}
the used lib signature pad by szimek
Used React and Bootstrap and some custome styles
the result would be
You didn't provide a full example, or much explanation of the code, so it's hard to tell what all is going on here, but I'll do my best to give as full an answer as I can.
Saving
First, if I understand the docs correctly, $(window).resize will be triggered at the same time as window.onresize. You use both. That might be causing some issues, maybe even the issues with saving.
The following code is run once, and I'm not sure what it's supposed to do:
var signature = $('#confirm_delete_signature').val();
if(signature){
signaturePad1.fromDataURL(signature);
}
var paraphe = $('#confirm_delete_paraphe').val();
if(paraphe){
signaturePad2.fromDataURL(paraphe);
}
It looks like it's supposed to be deleting the signature (since the selector is #confirm_delete_signature), but it instead, it's restoring a signature from some data stored in the node as a string. That might be causing issues too.
That said, I'm not sure why saving isn't working, but I can't find the code of your saving function, so it's very hard to say. Maybe I missed something.
I'm not familiar with php, sorry.
Resizing
For resizing, I think the React version that #Alexey Nikonov made might work with React (I didn't run it). You have to scale the positions of the points of the lines along with the changing size of the canvas.
I wanted a version closer to vanilla js, so I recreated it with just signature_pad v4.1.4 and jQuery at https://jsfiddle.net/j2Lurpd5/1/ (with an improvement to ratio calculation).
The code is as follows, though it doesn't have a button to clear the canvas:
<div id="wrapper">
<canvas id="pad" width="200" height="100"></canvas>
</div>
canvas {
border: red 1px solid;
}
// Inspiration: https://stackoverflow.com/a/60057521
// Version with no React
const canvas = document.querySelector('#pad');
const signPad = new SignaturePad(canvas);
// Doesn't work without the #wrapper. Probably because #pad
// needs it to be able to be 100% of it. Not sure exactly
// why that makes a difference when #wrapper doesn't have
// a width set on it. Though #pad alone does work after the
// first resize.
let prevWidth = $('#wrapper').width();
let lines = [];
setTimeout(resizeSignatureAndCanvas, 0);
window.addEventListener("resize", () => setTimeout(resizeSignatureAndCanvas, 0));
window.addEventListener("orientationchange", () => setTimeout(resizeSignatureAndCanvas, 0));
function resizeSignatureAndCanvas () {
// Get the current canvas contents
lines = signPad.toData();
// if there are no lines drawn, don't need to scale them
if ( signPad.isEmpty() ) {
// Set initial size
resizeCanvas();
} else {
// Calculate new size
let currentWidth = $('#wrapper').width();
let scale = currentWidth / prevWidth;
prevWidth = currentWidth; // Prepare for next time
// Scale the contents along with the width
setRescaledSignature(lines, scale);
// Match canvas to window size/device change
resizeCanvas();
// Load the adjusted canvas contents
signPad.fromData(lines);
}
};
// This is really the key to keeping the contents
// inside the canvas. Getting the scale right is important.
function setRescaledSignature (lines, scale) {
lines.forEach(line => {
line.points.forEach(point => {
// Same scale to avoid warping
point.x *= scale;
point.y *= scale;
});
});
};
function resizeCanvas () {
/** Have to resize manually to keep the canvas the width of the
* window without distorting the location of the "pen". */
// I'm not completely sure of everything in here
const canvas = $('#pad')[0];
// Not sure why we need both styles and props
canvas.style.width = '100%';
canvas.style.height = (canvas.offsetWidth / 1.75) + 'px';
// When zoomed out to less than 100%, for some very strange reason,
// some browsers report devicePixelRatio as less than 1
// and only part of the canvas is cleared then.
let ratio = Math.max(window.devicePixelRatio || 1, 1);
// This part causes the canvas to be cleared
canvas.width = canvas.offsetWidth * ratio;
canvas.height = canvas.offsetHeight * ratio;
canvas.getContext("2d").scale(ratio, ratio);
};
As you can see from my notes, I'm not completely sure why every part works, but from what I can tell it does preserve the behavior of the version that #Alexey Nikonov made.

Convert SVG to PNG and maintain CSS integrity

I am currently using canvg() and Canvas2Image to copy my SVG to a canvas and then convert the canvas to PNG. I would like to maintain the image format and not use PDF.
How can I maintain the CSS integrity? Chart is made using NVD3.js.
downloadPhoto: function() {
var chartArea = document.getElementsByTagName('svg')[0].parentNode;
var svg = chartArea.innerHTML;
var canvas = document.createElement('canvas');
canvas.setAttribute('width', chartArea.offsetWidth);
canvas.setAttribute('height', chartArea.offsetHeight);
canvas.setAttribute('display', 'none');
canvas.setAttribute(
'style',
'position: absolute; ' +
'top: ' + (-chartArea.offsetHeight * 2) + 'px;' +
'left: ' + (-chartArea.offsetWidth * 2) + 'px;');
document.body.appendChild(canvas);
canvg(canvas, svg);
Canvas2Image.saveAsPNG(canvas);
canvas.parentNode.removeChild(canvas);
}
Style definitions for svg elements defined in stylesheets are not applied to the generated canvas. This can be patched by adding style definitions to the svg elements before calling canvg.
Inspired on this article, I've created this:
function generateStyleDefs(svgDomElement) {
var styleDefs = "";
var sheets = document.styleSheets;
for (var i = 0; i < sheets.length; i++) {
var rules = sheets[i].cssRules;
for (var j = 0; j < rules.length; j++) {
var rule = rules[j];
if (rule.style) {
var selectorText = rule.selectorText;
var elems = svgDomElement.querySelectorAll(selectorText);
if (elems.length) {
styleDefs += selectorText + " { " + rule.style.cssText + " }\n";
}
}
}
}
var s = document.createElement('style');
s.setAttribute('type', 'text/css');
s.innerHTML = "<![CDATA[\n" + styleDefs + "\n]]>";
//somehow cdata section doesn't always work; you could use this instead:
//s.innerHTML = styleDefs;
var defs = document.createElement('defs');
defs.appendChild(s);
svgDomElement.insertBefore(defs, svgDomElement.firstChild);
}
// generate style definitions on the svg element(s)
generateStyleDefs(document.getElementById('svgElementId'));
The key thing here is that all the style rules need to be part of the SVG, not in external style files. So you would need to go through all the CSS for NVD3 and set all of those attributes in the code. Anything that is set via an external stylesheet will be ignored.
just to make #Lars Kotthoff's answer more concrete. "example of how to export a png directly from an svg" has a working example. the code snippet/gist tries to first apply all css to the svg inline and then draw the image on the canvas and export the data as png. (internally it adopted svg-crowbar code). and i apply the technique in my project and it works smoothly - a download button that can download the svg image rendered using nvd3.

how to scale my textlayer

i wish to scale a text layer by button click.
var messageLayer = new Kinetic.Layer();
function writeMessage(message, xx, yy) {
var context = messageLayer.getContext();
context.font = '12pt Calibri';
context.fillStyle = 'white';
wrapText(context, message, xx, yy, seat_size, seat_size);
}
stage.add(messageLayer);
writeMessage(text, xx, yy);
document.getElementById('zoomInBtn').onclick = function zoomInStage(){
stage.setScale(2);
stage.draw();
}
I'm not sure why but this seems not working..
Thanks for helping.
Hi thanks for pointing out my mistake.
but after i add the y value and this is still not working ....
below are my code:
enter <!DOCTYPE HTML>
<html>
<body>
<div id="container"></div>
<button id="scaleme" style="margin-left:10px;">Scale</button>
<script src="http://www.html5canvastutorials.com/libraries/kinetic-v4.3.0-beta2.js"></script>
<script>
function writeMessage(messageLayer, message) {
var context = messageLayer.getContext();
messageLayer.clear();
context.font = '18pt Calibri';
context.fillStyle = 'black';
context.fillText(message, 10, 25);
}
var stage = new Kinetic.Stage({
container: 'container',
width: 578,
height: 200
});
var messageLayer = new Kinetic.Layer();
stage.add(messageLayer);
writeMessage(messageLayer, 'text text text');
document.getElementById('scaleme').onclick = function scale(){
messageLayer.setScale(2,2);
}
</script>
</body>
</html>
please helppp....thanks!
From the API docs:
this is the format:
setScale(x, y)
So you are missing the y parameter it seems.
If you do
stage.setScale(2,2);
then the whole stage will be scaled, but if you want just the text layer to be scaled:
messageLayer.setScale(2,2);
lastly, if you want to be fancy, you can do a transition to zoom to the text:
messageLayer.on('click', function(){
messageLayer.transitionTo({
scale: {
x: 2,
y: 2
},
duration: 1,
easing: 'elastic-ease-out' // this one is not really necessary, just for show
});
});
EDIT:
Since you are using canvas primitives, do this: http://jsfiddle.net/mmqXK/
var context = messageLayer.getContext();
context.scale(1, 0.5);
messageLayer.draw();
writeMessage(messageLayer, 'text text text');

Resources