jsPDF is not exporting graphics - primefaces

Hello I am developing a page in jsf + primefaces that I need to generate a report of some graphs that are displayed on it, I'm using jsPDF but I can not export the graph to pdf, it only displays the graph string, but the graph that is in page is not being exported
<ui:define name="body" >
<h:form acceptcharset="ISO-8859-1" lang="pt_BR" class="form-pesquisa">
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/1.3.5/jspdf.min.js"></script>
<script>
function gerarPDF() {
var pdf = new jsPDF('p', 'pt', 'letter');
// source can be HTML-formatted string, or a reference
// to an actual DOM element from which the text will be scraped.
source = $('#gerarPdf')[0];
alert(source);
// we support special element handlers. Register them with jQuery-style
// ID selector for either ID or node name. ("#iAmID", "div", "span" etc.)
// There is no support for any other type of selectors
// (class, of compound) at this time.
specialElementHandlers = {
// element with id of "bypass" - jQuery style selector
'#bypassme': function (element, renderer) {
// true = "handled elsewhere, bypass text extraction"
return true
}
};
margins = {
top: 80,
bottom: 60,
left: 40,
width: 522
};
// all coords and widths are in jsPDF instance's declared units
// 'inches' in this case
pdf.fromHTML(
source, // HTML string or DOM elem ref.
margins.left, // x coord
margins.top, { // y coord
'width': margins.width, // max width of content on PDF
'elementHandlers': specialElementHandlers
},
function (dispose) {
// dispose: object with X, Y of the last line add to the PDF
// this allow the insertion of new lines after html
pdf.save('Test.pdf');
}, margins);
}
</script>
<div id="gerarPdf">
<div class="form-content">
Grafico
<div class="form-row">
<div class="form-group col-xs-12" style="text-align:center;">
<p:lineChart model="#{relatorioIndividualController.lineModel}" style="width: 100%; height: 500px;"/>
</div>
</div>
</div>
</div>
<h:commandButton value="PDF" id="btnPDF" onclick="gerarPDF();"/>
</h:form>
</ui:define>
page
Img Page
After exportation
After exportation
Any ideas why it doesn't export the graph ?, do I have to use some other jsPDF property?

Related

Using Angular 9, How to print jspdf from html element while updating values in the html using interpolation

Html template
<div id="download">
<div>
{{name}}
</div>
</div>
<button type="button" class="btn btn-primary" (click)="captureScreen()">Download</button>
Component.ts:
public name = 'Sample';
captureScreen() {
this.name = "something else";
var img;
var newImage;
let filename = 'mypdf_'+'.pdf';
var node = document.getElementById('download');
domtoimage.toPng(node, { bgcolor: '#fff' }).then(function (dataUrl) {
img = new Image();
img.src = dataUrl;
newImage = img.src;
img.onload = function () {
var pdfWidth = img.width;
var pdfHeight = img.height;
var doc;
doc = new jspdf('l', 'px', [pdfWidth, pdfHeight]);
var width = doc.internal.pageSize.getWidth();
var height = doc.internal.pageSize.getHeight();
doc.addImage(newImage, 'PNG', 10, 10, width, height);
doc.save(filename);
};
}).catch(function (error) { });
}
This code works fine for a pre-set value in the name field.
I want to loop through a list of names and print a new page for every name in the pdf. But currently, if I change the value
this.name = "something else";
it still outputs the name as 'Sample' in the pdf.
How do I change the value of name on the go and print pdf for different names?
1) Issue is that you are trying to use document.getElementById('download') to query a new element which has been rendered yet, it's still rendering the old element. You need to make sure render new element first.
2) Recommend to query element by using ViewChild in angular.
Here is my suggestion
<!-- Use element reference #download to query native element -->
<div #download>
<div>
{{name}}
</div>
</div>
<input type="text" (keyup)="changeElementText($event.target.value)">
<button type="button" class="btn btn-primary"
(click)="captureScreen()">Download</button>
In ts file
name = 'Sample';
#ViewChild('download', {read: ElementRef})
download: ElementRef;
// Add a modify element logic, make sure modify element and render it before capture screen.
changeElementText(value: string) {
this.name = value;
}
captureScreen() {
let img;
let newImage;
const filename = 'mypdf_'+'.pdf';
// This should provide you latest element.
const node = this.download.nativeElement;
console.log(node);
/*** Your capture logic ***/
}
stackbliz demo

Extending native HTML element in Angular 6

I have recently created a native web component which is working well in all browsers. I moved this web component into an Angular 6 application and all works as expected. I then tried to extend a native HTML element which again worked perfectly except when I brought it into my Angular 6 application.
Using the examples from Mozilla I will try and illustrate my issue. Using the following trying to extend a native 'p' element:
// Create a class for the element
class WordCount extends HTMLParagraphElement {
constructor() {
// Always call super first in constructor
super();
// count words in element's parent element
var wcParent = this.parentNode;
function countWords(node){
var text = node.innerText || node.textContent
return text.split(/\s+/g).length;
}
var count = 'Words: ' + countWords(wcParent);
// Create a shadow root
var shadow = this.attachShadow({mode: 'open'});
// Create text node and add word count to it
var text = document.createElement('span');
text.textContent = count;
// Append it to the shadow root
shadow.appendChild(text);
// Update count when element content changes
setInterval(function() {
var count = 'Words: ' + countWords(wcParent);
text.textContent = count;
}, 200)
}
}
// Define the new element
customElements.define('word-count', WordCount, { extends: 'p' });
<p is="word-count">This is some text</p>
By taking that same code and putting it into an Angular 6 application, the component never runs. I put console log statements in the constructor and connectedCallback methods and they never trigger. If I remove the {extends: 'p'} object and change the extends HTMLParagraphElement and make it an extend HTMLElement to be an autonomous custom element everything works beautifully. Am I doing something wrong or does Angular 6 not support the customized built-in element extension?
I assume the reason is the way that Angular creates those customized built-in elements when parsing component templates - it probably does not know how to properly do that. Odds are it considers is a regular attribute which is fine to add after creation of the element (which it isn't).
First creating the element and then adding the is-attribute will unfortunately not upgrade the element.
See below example: div#d has a non-working example of that customized input.
customElements.define('my-input', class extends HTMLInputElement {
connectedCallback() {
this.value = this.parentNode.id
this.parentNode.classList.add('connected')
}
}, {
extends: 'input'
})
document.addEventListener('DOMContentLoaded', () => {
b.innerHTML = `<input type="text" is="my-input">`
let el = document.createElement('input', {
is: 'my-input'
})
el.type = 'text'
c.appendChild(el)
// will not work:
let el2 = document.createElement('input')
el2.setAttribute('is', 'my-input')
el2.type = 'text'
d.appendChild(el2)
})
div {
border: 3px dotted #999;
padding: 10px;
}
div::before {
content: "#"attr(id)" ";
}
.connected {
background-color: lime;
}
<div id="a"><input type="text" is="my-input"></div>
<div id="b"></div>
<div id="c"></div>
<div id="d"></div>
So to get it to work with Angular, hook into the lifecycle of your Angular component (e.g. onInit() callback) and pick a working way to create your element there.

Get incorrect offsetWidth and offsetHeight values

Here is my angular2 code.
Template
<div #picker class="slider">
<div class="slider-track">
<div #sliderSelectionEl class="slider-selection"></div>
<div #sliderHandle1 class="slider-handle"></div>
<div #sliderHandle2 class="slider-handle"></div>
</div>
<div #tooltipEl class="tooltip">
<div class="tooltip-arrow"></div>
<div #tooltipInner class="tooltip-inner"></div>
</div>
<input type="text" class="span2" value="" id="sl2"><br/>
</div>
Component
import {Component, OnInit, Input, ViewChild, ElementRef, Renderer} from '#angular/core';
export class SliderComponent implements OnInit {
#ViewChild('picker') picker: ElementRef;
constructor(private renderer: Renderer, private el: ElementRef) {
}
ngAfterViewInit() {
this.renderer.setElementClass(this.picker.nativeElement, 'slider-horizontal', true);
console.log(this.picker.nativeElement.offsetWidth);
console.log(this.picker.nativeElement.offsetHeight);
}
}
.slider-horizontal {
width: 210px;
height: 20px;
}
The problem is the printed values are different for each time loading. I guess this issue is due to the browser have not completed loading the div. Do you know what is the solution for this?
You can detect size changes by using
MutationObserver
Probably the biggest audience for this new api are the people that
write JS frameworks, [...] Another use case would be situations where you are using frameworks that manipulate the DOM and need to react to these
modifications efficiently ( and without setTimeout hacks! ).
Here is how you can use it to detect changes in elements :
// select the target node
var target = document.querySelector('#some-id'); // or
// create an observer instance
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
console.log(mutation.type);
});
});
// configuration of the observer:
var config = { attributes: true, childList: true, characterData: true }
// pass in the target node, as well as the observer options
observer.observe(target, config);
// later, you can stop observing
observer.disconnect();
For your case, you could use it inside your ngAfterViewInit and refresh your offsets size. You can be more specific and only detect some mutations, and only then extract your offsets.
more info :
doc: https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver
compatibility : https://caniuse.com/#feat=mutationobserver
Demo:
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
console.log(mutation);
if(mutation.attributeName == 'class') // detect class change
/*
or if(mutation.target.clientWidth == myWidth)
*/
showOffset(mutation.target);
observer.disconnect();
});
});
var config = { attributes: true}
var demoDiv = document.getElementById('demoDiv');
var logs = document.getElementById('logs');
// wait for document state to be complete
if (document.readyState === "complete") {
ngAfterViewInit();
}
document.onreadystatechange = function () {
if (document.readyState === "complete") {
ngAfterViewInit();
}
}
// observe changes that effects demoDiv + add class
function ngAfterViewInit(){
observer.observe(demoDiv, config);
demoDiv.classList.add('slider-horizontal');
}
// show offsetWidth + height.
// N.B offset width and height will be bigger than clientWidth because I added a border. If you remove the border you'll see 220px,20px
function showOffset(element){
offsetMessage = "offsetWidth:" + demoDiv.offsetWidth + " offsetHeight: " + demoDiv.offsetHeight;
console.log(offsetMessage);
logs.innerHTML = offsetMessage;
}
.slider-horizontal {
border: 2px solid red;
width: 210px;
height: 20px;
background: grey;
}
<div id='demoDiv'> I am a demo div </div>
<div style="margin-top: 20px;"> logs : <span id='logs' style=" padding: 5px; border: 1px solid black"></span></div>
You have to schedule calls to 'offsetWidth' after rendering cycle, angular executes draw on the end of microtask queue, so you could try setTimeout(..., 0) or run Promise.resolve().then(...) outside of zonejs. Hope it helps.
In order to get correct offset values you can use: ngAfterContentChecked with the AfterContentChecked Interface.
This method is called after every change detection run. So, inside this method use a flag (or counter) and setTimeOut:
if (this.counter <= 10) {
// this print offsetwidth of my element
console.log('mm ' + this.container.nativeElement.offsetWidth);
// setTimeOut allow to run another changedetection
// so ngAfterContentChecked will run again
setTimeout(() => { }, 0);
//you could use a counter or a flag in order to stop getting the right width
this.counter++;
}
Hope it helps! Feel free to comment

Nvd3 chart huge with oficial Css

I am having unexpected problems with a couple of Nvd3 charts. I coded them withouht using the nvd3 css file (nv.d3.min.css). Without it everything was ok but when I added it suddendly the second chart took a lot of space (1500x1500). The normal size was 450x450 but now it is
If i look in the console of chrome and uncheck the style atributes "width: 100%;" and "height: 100%;" it works (actually with only one). The other thing that changes de css atributes is the "user agent stylesheet".
I canĀ“t understand why because i thought that the size was explicitely coded while the configuration of the chart
HTML
<div id="charts">
<div id="piechart" ><svg></svg></div>
<div id="chart"><svg></svg></div>
</div>
NVD3
function setupGraph(data_graph) {
nv.addGraph(function() {
var pieChart = nv.models.pieChart();
pieChart.margin({top: 30, right: 60, bottom: 20, left: 60});
var datum = data_graph[0].values;
pieChart.tooltipContent(function(key, y, e, graph) {
var x = String(key);
var y = String(y);
tooltip_str = '<center><b>'+x+'</b></center>' + y;
return tooltip_str;
});
pieChart.showLabels(true);
pieChart.donut(false);
pieChart.showLegend(true);
pieChart
.x(function(d) { return d.label })
.y(function(d) { return d.value });
pieChart.width(450);
pieChart.height(450);
d3.select('#piechart svg')
.datum(datum)
.transition().duration(350)
.attr('width',450)
.attr('height',450)
.call(pieChart);
nv.utils.windowResize(chart.update);
return chart;
});
}
function setupGraph2(data_graph) {
nv.addGraph(function() {
var chart = nv.models.discreteBarChart()
.x(function(d) { return d.label }) //Specify the data accessors.
.y(function(d) { return d.value })
//.valueFormat(d3.format(',.2f'))
.staggerLabels(true) //Too many bars and not enough room? Try staggering labels.
.tooltips(false) //Don't show tooltips
.showValues(true) //...instead, show the bar value right on top of each bar.
.transitionDuration(350)
;
chart.width(450);
chart.height(450);
d3.select('#chart svg')
.datum(data_graph)
.attr('width',450)
.attr('height', 450)
.call(chart);
nv.utils.windowResize(chart.update);
return chart;
});
Can anybody see what is happening?
Just override the default width and height properties of the nvd3.css stylesheet, creating a rule in your stylesheet, and ensuring it is loaded after the nvd3 stylesheet.
The last rule (with the same specificity) wins:
svg {
width : auto;
height : auto;
}
or create a more specific rule to act on your svgs only, like:
#charts svg {
width : 450px;
height : 450px;
}

Layer Ordering in leaflet.js

How can I force a new layer added to the map in Leaflet to be the first over the basemap?
I could not find a method to easily change the order of the layers, which is a very basic GIS feature. Am I missing something?
A Leaflet map consists of a collection of "Panes" whose view order is controlled using z-index. Each pane contains a collection of Layers The default pane display order is tiles->shadows->overlays->markers->popups. Like Etienne described, you can control the display order of Paths within the overlays pane by calling bringToFront() or bringToBack(). L.FeatureGroup also has these methods so you can change the order of groups of overlays at once if you need to.
If you want to change the display order of a whole pane then you just change the z-index of the pane using CSS.
If you want to add a new Map pane...well I'm not sure how to do that yet.
http://leafletjs.com/reference.html#map-panes
http://leafletjs.com/reference.html#featuregroup
According to Leaflet API, you can use bringToFront or bringToBack on any layers to brings that layer to the top or bottom of all path layers.
Etienne
For a bit more detail, Bobby Sudekum put together a fantastic demo showing manipulation of pane z-index. I use it as a starting point all the time.
Here's the key code:
var topPane = L.DomUtil.create('div', 'leaflet-top-pane', map.getPanes().mapPane);
var topLayer = L.mapbox.tileLayer('bobbysud.map-3inxc2p4').addTo(map);
topPane.appendChild(topLayer.getContainer());
topLayer.setZIndex(7);
Had to solve this recently, but stumbled upon this question.
Here is a solution that does not rely on CSS hacks and works with layer groups. It essentially removes and re-adds layers in the desired order.
I submit this as a better "best practice" than the current answer. It shows how to manage the layers and re-order them, which is also useful for other contexts. The current method uses the layer Title to identify which layer to re-order, but you can easily modify it to use an index or a reference to the actual layer object.
Improvements, comments, and edits are welcome and encouraged.
JS Fiddle: http://jsfiddle.net/ob1h4uLm/
Or scroll down and click "Run code snippet" and play with it. I set the initial zoom level to a point that should help illustrate the layerGroup overlap effect.
function LeafletHelper() {
// Create the map
var map = L.map('map').setView([39.5, -0.5], 4);
// Set up the OSM layer
var baseLayer = L.tileLayer(
'http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 18
}).addTo(map);
var baseLayers = {
"OSM tiles": baseLayer
};
this.map = map;
this.BaseLayers = {
"OSM tiles": baseLayer
};
this.LayersControl = L.control.layers(baseLayers).addTo(map);
this.Overlays = [];
this.AddOverlay = function (layerOptions, markers) {
var zIndex = this.Overlays.length;
var layerGroup = L.layerGroup(markers).addTo(map);
this.LayersControl.addOverlay(layerGroup, layerOptions.title);
this.Overlays.push({
zIndex: zIndex,
LeafletLayer: layerGroup,
Options: layerOptions,
InitialMarkers: markers,
Title: layerOptions.title
});
return layerGroup;
}
this.RemoveOverlays = function () {
for (var i = 0, len = this.Overlays.length; i < len; i++) {
var layer = this.Overlays[i].LeafletLayer;
this.map.removeLayer(layer);
this.LayersControl.removeLayer(layer);
}
this.Overlays = [];
}
this.SetZIndexByTitle = function (title, zIndex) {
var _this = this;
// remove overlays, order them, and re-add in order
var overlays = this.Overlays; // save reference
this.RemoveOverlays();
this.Overlays = overlays; // restore reference
// filter overlays and set zIndex (may be multiple if dup title)
overlays.forEach(function (item, idx, arr) {
if (item.Title === title) {
item.zIndex = zIndex;
}
});
// sort by zIndex ASC
overlays.sort(function (a, b) {
return a.zIndex - b.zIndex;
});
// re-add overlays to map and layers control
overlays.forEach(function (item, idx, arr) {
item.LeafletLayer.addTo(_this.map);
_this.LayersControl.addOverlay(item.LeafletLayer, item.Title);
});
}
}
window.helper = new LeafletHelper();
AddOverlays = function () {
// does not check for dups.. for simple example purposes only
helper.AddOverlay({
title: "Marker A"
}, [L.marker([36.83711, -2.464459]).bindPopup("Marker A")]);
helper.AddOverlay({
title: "Marker B"
}, [L.marker([36.83711, -3.464459]).bindPopup("Marker B")]);
helper.AddOverlay({
title: "Marker C"
}, [L.marker([36.83711, -4.464459]).bindPopup("Marker c")]);
helper.AddOverlay({
title: "Marker D"
}, [L.marker([36.83711, -5.464459]).bindPopup("Marker D")]);
}
AddOverlays();
var z = helper.Overlays.length;
ChangeZIndex = function () {
helper.SetZIndexByTitle(helper.Overlays[0].Title, z++);
}
ChangeZIndexAnim = function () {
StopAnim();
var stuff = ['A', 'B', 'C', 'D'];
var idx = 0;
var ms = 200;
window.tt = setInterval(function () {
var title = "Marker " + stuff[idx++ % stuff.length];
helper.SetZIndexByTitle(title, z++);
}, ms);
}
StopAnim = function () {
if (window.tt) clearInterval(window.tt);
}
#map {
height: 400px;
}
<link rel="stylesheet" type="text/css" href="http://cdn.leafletjs.com/leaflet-0.6.4/leaflet.css">
<script type='text/javascript' src="http://cdn.leafletjs.com/leaflet-0.6.4/leaflet.js"></script>
<div id="map"></div>
<input type='button' value='Remove overlays' onclick='helper.RemoveOverlays();' />
<input type='button' value='Add overlays' onclick='AddOverlays();' />
<input type='button' value='Move bottom marker to top' onclick='ChangeZIndex();' />
<input type='button' value='Change z Index (Animated)' onclick='ChangeZIndexAnim();' />
<input type='button' value='Stop animation' onclick='StopAnim();' />
I've found this fix (css):
.leaflet-map-pane {
z-index: 2 !important;
}
.leaflet-google-layer {
z-index: 1 !important;
}
found it here: https://gis.stackexchange.com/questions/44598/leaflet-google-map-baselayer-markers-not-visible