How to dynamically check remaining page height in PDFMake? - pdfmake

Is there a way to dynamically check remaining page height in PDFMake? When dynamically creating pages, I want to be able to check the remaining available page height to compare it to the element height, so that the last on page element (e.g. image or long textarea content) could not be cut but be transfered to another page instead. Do not know how to do it dynamically.

Thanks all of you. I finally used pageBreakBefore function, and headlineLevel which I use as a marker, and found a version of pdfmake which allows we to see if the node is an image, and thus I calculate the height of the element.
Here is how it looks in my code. There I also have a footer and have to consider it in my calculations so that the content should not go on it:
pageBreakBefore: function(currentNode, followingNodesOnPage, nodesOnNextPage, previousNodesOnPage) {
var pageInnerHeight = currentNode.startPosition.pageInnerHeight;
var top = (currentNode.startPosition.top) ? currentNode.startPosition.top : 0;
var footerHeight = 30;
var nodeHeight = 0;
if (followingNodesOnPage && followingNodesOnPage.length) {
nodeHeight = followingNodesOnPage[0].startPosition.top - top;
}
if (currentNode.headlineLevel === 'footer') return false;
return (currentNode.image && (top + nodeHeight + footerHeight > pageInnerHeight))
|| (currentNode.headlineLevel === 'longField' && (top + nodeHeight + footerHeight > pageInnerHeight))
|| currentNode.startPosition.verticalRatio >= 0.95;
}

Well, I might be a bit late. But in Version 0.1.17, they introduced the pageBreakBefore function.
Release Notes on Github
You can now specify a pageBreakBefore function, which can determine if a page break should be inserted before the page break. To implement a 'no orphan child' rule, this could like like this:
var dd = {
content: [
{text: '1 Headline', headlineLevel: 1},
'Some long text of variable length ...',
{text: '2 Headline', headlineLevel: 1},
'Some long text of variable length ...',
{text: '3 Headline', headlineLevel: 1},
'Some long text of variable length ...',
],
pageBreakBefore: function(currentNode, followingNodesOnPage, nodesOnNextPage, previousNodesOnPage) {
return currentNode.headlineLevel === 1 && followingNodesOnPage.length === 0;
}
}
If pageBreakBefore returns true, a page break will be added before the currentNode. Current node has the following information attached:
{
id: '<as specified in doc definition>',
headlineLevel: '<as specified in doc definition>',
text: '<as specified in doc definition>',
ul: '<as specified in doc definition>',
ol: '<as specified in doc definition>',
table: '<as specified in doc definition>',
image: '<as specified in doc definition>',
qr: '<as specified in doc definition>',
canvas: '<as specified in doc definition>',
columns: '<as specified in doc definition>',
style: '<as specified in doc definition>',
pageOrientation '<as specified in doc definition>',
pageNumbers: [2, 3], // The pages this element is visible on (e.g. multi-line text could be on more than one page)
pages: 6, // the total number of pages of this document
stack: false, // if this is an element which encapsulates multiple sub-objects
startPosition: {
pageNumber: 2, // the page this node starts on
pageOrientation: 'landscape', // the orientation of this page
left: 60, // the left position
right: 60, // the right position
verticalRatio: 0.2, // the ratio of space used vertically in this document (excluding margins)
horizontalRatio: 0.0 // the ratio of space used horizontally in this document (excluding margins)
}
}

I recently talked to a colleague on a dev congress. They faced the same problem. If you really need to know there are 2 possibilities as far as I know:
1) Test render the page data and check if output is more than one page. This is derpy but you do not know internals.
2) Do the calculations which are done within pdfmake on your own before generating the pdf. On how to you need to look into the pdfmake generation code itself unfortunately.
If there is a more elegant solution I'd very much like to know myself!

I am doing it manually. All you need is to know what is the max size of the page, before text starts getting out. In my case, Legal paper size has max width of 700px before text gets truncated.
So what I do is to reduce the column widths in a loop until totalPageWidth is less than acceptableWidth.
It may not be very readable, but here is the code.
// For a legal size page, total width is 700. So try and push all columns within 700
// Following lines are there to reduce the width of columns so as to adjust the total width.
// Width is deducted for every column so as not to affect any individual column.
totalOutOfPageWidth = totalWidth - 700;
var totalWidthDeducted = 0;
while (totalOutOfPageWidth > 0) {
for (var c = 0; c < colWidthArray.length; c++) {
if (totalOutOfPageWidth > 0) {
if (colWidthArray[c] == width70 - totalWidthDeducted) {
colWidthArray[c] = colWidthArray[c] - 5;
totalOutOfPageWidth -= 5;
}
}
}
if (totalOutOfPageWidth > 0) {
for (var c = 0; c < colWidthArray.length; c++) {
if (colWidthArray[c] == width50 - totalWidthDeducted) {
colWidthArray[c] = colWidthArray[c] - 5;
totalOutOfPageWidth -= 5;
}
}
}
if (totalOutOfPageWidth > 0) {
for (var c = 0; c < colWidthArray.length; c++) {
if (colWidthArray[c] == width35 - totalWidthDeducted) {
colWidthArray[c] = colWidthArray[c] - 5;
totalOutOfPageWidth -= 5;
}
}
}
if (totalOutOfPageWidth > 0) {
for (var c = 0; c < colWidthArray.length; c++) {
if (colWidthArray[c] == width25 - totalWidthDeducted) {
colWidthArray[c] = colWidthArray[c] - 5;
totalOutOfPageWidth -= 5;
}
}
}
totalWidthDeducted += 5;

Related

Highcharts : Set an y offset to the series

Situation : Chart with some analogic-series and some digital-series (0-1).
The problem is with the digital series. I would like to make sure that the series do not overlap like the image 1.
My idea is to set an "y-offset" on the digital-series, to have a result like the image 2.
This is a part of the y Axis configuration of the digital series. All the digital-series is related to a single y Axis (with id digital).
id : "digital",
min: 0,
max : 1,
ceiling:1,
floor : 0,
tickInterval: 1
Image 1.
Image 2.
In the documentation i can't find anything that can help me. And this is not my case.
UPDATE
Example on JSFIDDLE. Look (yes it's impossible currently) at the digital series with green color.
If you add a function in the load event, you can change the value of y in a way that makes it look offset. Like this:
chart: {
events: {
load: function() {
var series = this.series;
for (var i = 0; i < series.length; i++) {
if (series[i].yAxis.userOptions.id == "digital") {
for (var j = 0; j < series[i].data.length; j++) {
if (series[i].data[j].y == 1) {
series[i].data[j].y = 1 + 0.1 * i;
} else {
series[i].data[j].y = 0
}
}
this.update(series, true, false);
}
}
}
}
}
This sets the new y value equal to 0.1 * the series index.
Working example: https://jsfiddle.net/u2pzrhgk/27/

pdfMake - Wide Table Page Break

I am using pdfMake to generate table reports. Some of the reports are very wide and dont fit on a standard page width, even in landscape mode. Currently, pdfMake is cutting off the table content when it overflows past the page margin.
I would like to page break the table when it is too wide, much like when the rows overflow to the next page.
Is this possible using pdfMake?
Can using pageBreakBefore callback function help for this?
Thank you
Yes, this is possible with pdfMake, even though not currently a feature.
To achieve this, you can just break overflowing columns into another table. We can put all the tables in an array, then just set them in the docDefinition as follows.
Any common attributes you want in the tables can be defined in the Template constructor.
for (var i = 0; i < tables.length;i++) {
docDefinition.content[i] = new Template();
docDefinition.content[i].table.body = tables[i];
docDefinition.content[i].table.widths = widths[i];
if (i > 0) {
docDefinition.content[i].pageBreak = 'before';
}
}
function Template(){
this.table = {
dontBreakRows: true
};
//zebra stripes layout
this.layout = {
fillColor: function (row, node, col) {
return (row % 2 === 0) ? '#CCCCCC' : null;
}
}
}
How do we determine if a column will overflow? We have two options:
If you are using bootstrap datatables, you can use the "width" attribute in the html.
pdfmake calculates the actual width, but you may have to dig around in pdfmake.js.
Then, just loop through, adding widths until you exceed your limit (my limit was for 8pt font). You can do this for THs then save those column splits and use those for the TDs.
If the final page is just barely overflowing, we don't want the final page to have just one column, we want each page to have roughly the same width. We calculate the number of pages needed, then find the desired break point from there. To link the pages together more easily, you can add a row number column at the beginning of each table.
var colSplits = [];
var tables = new Array();
function parseTHs(colSplits, tables) {
var colSum = 0;
var pageSize = 1120-7*rows.toString().length;
var paddingDiff = 11.9;
var columns = 0;
var prevSum;
var i = 0;
var width = $(".dataTables_scrollHeadInner > table").width();
var pages = Math.ceil(width/pageSize);
console.log("pages: "+pages);
var desiredBreakPoint = width/pages;
console.log("spread: "+desiredBreakPoint);
var limit = pageSize;
var row = ['#'];
var percent = '%';
widths.push(percent);
$(".dataTables_scrollHeadInner > table > thead > tr:first > th").each(function() {
prevSum = colSum;
colSum += $(this).outerWidth()-paddingDiff;
//if adding column brings us farther away from the desiredBreakPoint than before, kick it to next table
if (colSum > limit || desiredBreakPoint-colSum > desiredBreakPoint-prevSum) {
tables[i] = [row];
row = ['#'];
widths.push(percent);
colSplits.push(columns);
i++;
desiredBreakPoint += width/pages;
limit = prevSum+pageSize;
}
row.push({text: $(this).text(), style:'header'});
widths.push(percent);
columns++;
});
//add the final set of columns
tables[i] = [row];
}
function parseTDs(colSplits, tables) {
var currentRow = 0;
$("#"+tableId+" > tbody > tr").each(function() {
var i = 0;
var row = [currentRow+1];
var currentColumn = 0;
var split = colSplits[i];
$(this).find("td").each(function() {
if (currentColumn === split) {
tables[i].push(row);
row = [currentRow+1];
i++;
split = colSplits[i];
}
row.push({text: $(this).text()});
currentColumn++;
});
//add the final set of columns
tables[i].push(row);
currentRow++;
});
}
parseTHs(colSplits, tables);
parseTDs(colSplits, tables);
Note: If you want the columns to fill all the available page, there's a good implementation for that at this link.
I just added '%' for the widths and added that code to pdfmake.js.
Hope this helps!
Just add dontBreakRows property in your table object like this
table: {
dontBreakRows: true,
widths: [30,75,48,48,48,48,48,115],
body: []
}
Also, you can make the page wider and change the page orientation as landscape.
pageSize: "A2",
pageOrientation: "landscape",

Word wrap in generated PDF (using jsPDF)?

what I'm doing is using jsPDF to create a PDF of the graph I generated. However, I am not sure how to wrap the title (added by using the text() function). The length of the title will vary from graph to graph. Currently, my titles are running off the page. Any help would be appreciated!
This is the code i have so far:
var doc = new jsPDF();
doc.setFontSize(18);
doc.text(15, 15, reportTitle);
doc.addImage(outputURL, 'JPEG', 15, 40, 180, 100);
doc.save(reportTitle);
Nothing to keep the reportTitle from running off the page
Okay I've solved this. I used the jsPDF function, splitTextToSize(text, maxlen, options). This function returns an array of strings. Fortunately, the jsPDF text() function, which is used to write to the document, accepts both strings and arrays of strings.
var splitTitle = doc.splitTextToSize(reportTitle, 180);
doc.text(15, 20, splitTitle);
You can just use the optional argument maxWidth from the text function.
doc.text(15, 15, reportTitle, { maxWidth: 40 });
That will split the text once it reaches the maxWidth and start on the next line.
Auto-paging and text wrap issue in JSPDF can achieve with following code
var splitTitle = doc.splitTextToSize($('#textarea').val(), 270);
var pageHeight = doc.internal.pageSize.height;
doc.setFontType("normal");
doc.setFontSize("11");
var y = 7;
for (var i = 0; i < splitTitle.length; i++) {
if (y > 280) {
y = 10;
doc.addPage();
}
doc.text(15, y, splitTitle[i]);
y = y + 7;
}
doc.save('my.pdf');
To wrap long string of text to page use this code:
var line = 25 // Line height to start text at
var lineHeight = 5
var leftMargin = 20
var wrapWidth = 180
var longString = 'Long text string goes here'
var splitText = doc.splitTextToSize(longString, wrapWidth)
for (var i = 0, length = splitText.length; i < length; i++) {
// loop thru each line and increase
doc.text(splitText[i], leftMargin, line)
line = lineHeight + line
}
If you need to dynamically add new lines you want to access the array returned by doc.splitTextToSize and then add more vertical space as you go through each line:
var y = 0, lengthOfPage = 500, text = [a bunch of text elements];
//looping thru each text item
for(var i = 0, textlength = text.length ; i < textlength ; i++) {
var splitTitle = doc.splitTextToSize(text[i], lengthOfPage);
//loop thru each line and output while increasing the vertical space
for(var c = 0, stlength = splitTitle.length ; c < stlength ; c++){
doc.text(y, 20, splitTitle[c]);
y = y + 10;
}
}
Working Helper function
Here's a complete helper function based on the answers by #KB1788 and #user3749946:
It includes line wrap, page wrap, and some styling control:
(Gist available here)
function addWrappedText({text, textWidth, doc, fontSize = 10, fontType = 'normal', lineSpacing = 7, xPosition = 10, initialYPosition = 10, pageWrapInitialYPosition = 10}) {
var textLines = doc.splitTextToSize(text, textWidth); // Split the text into lines
var pageHeight = doc.internal.pageSize.height; // Get page height, well use this for auto-paging
doc.setFontType(fontType);
doc.setFontSize(fontSize);
var cursorY = initialYPosition;
textLines.forEach(lineText => {
if (cursorY > pageHeight) { // Auto-paging
doc.addPage();
cursorY = pageWrapInitialYPosition;
}
doc.text(xPosition, cursorY, lineText);
cursorY += lineSpacing;
})
}
Usage
// All values are jsPDF global units (default unit type is `px`)
const doc = new jsPDF();
addWrappedText({
text: "'Twas brillig, and the slithy toves...", // Put a really long string here
textWidth: 220,
doc,
// Optional
fontSize: '12',
fontType: 'normal',
lineSpacing: 7, // Space between lines
xPosition: 10, // Text offset from left of document
initialYPosition: 30, // Initial offset from top of document; set based on prior objects in document
pageWrapInitialYPosition: 10 // Initial offset from top of document when page-wrapping
});
When we use linebreak in jsPDF we get an error stating b.match is not defined, to solve this error just unminify the js and replace b.match with String(b).match and u will get this error twice just replace both and then we get c.split is not defined just do the same in this case replace it with String(c).match and we are done. Now you can see line breaks in you pdf. Thank you

highcharts: pie chart - reduce number of slice

I have a pie chart with so many slices that is very hard to read it. Is it possible to reduce the number of slices, by grouping the smallest in just one named "others", or hiding them?
No. This behaviour is not built into highcharts.
The easiest way to achieve this is by manually changing the data you pass to the chart. Ie if you do the grouping into a category 'Others' before you pass the data and render the chart
Pasting some info in here as a pointer for people that would like to do the above with javascript outside of Highcharts, like i did myself.
for(i=0; i<dataJSON.finished.length; i++) {
//console.info(i);
if(dataJSON.finished[i].name !== '_all_' && dataJSON.finished[i].name !== 'Anders') {
tempValue=0;
for(j=0; j<dataJSON.finished[i].data.length; j++) { tempValue += dataJSON.finished[i].data[j]; }
if(tempValue / totalValue > 0.02) {
pieData.push({ name:dataJSON.finished[i].name, y:tempValue });
} else andersValue += tempValue;
}
}
//console.info(pieData);
pieData.sort(function(a,b) {return (a.y > b.y) ? -1 : ((b.y > a.y) ? 1 : 0);});
pieData.push({ name: "Overig", y: andersValue });

Set min/max for each range handle in jQuery UI slider?

I'm using a jQuery slider where users can select a time range between 00:00 and 1d+12:00. 36 hours all together.
Anyway.
I would like to apply min and max values to my handles based on what they're set to. These are my requirements:
left handle can never go over midnight on the next day (max is 24 hours)
left handle can never go more left than -24 hours from right handle (min is right handle value minus 24 hours)
right handle can never go more than +24 hours from the left handle (max is left handle value plus 24 hours)
As I understand, minimum and maximum values can only be applied to single handle slider control and not to range slider?
Is it possible to set minimums and maximums individually to both handles?
I've tried initializing it this way but no luck:
$(".timing-slider", timing).slider({
range: true,
min: [0, 0],
max: [24, 36],
}
This jQuery UI slider extension satisfies all upper requirements
I've managed to change default jQuery UI slider to include a few more configuration properties:
minRangeSize - sets minimum range size so ranges can't be narrower than this setting
maxRangeSize - sets maximum range size so ranges can't be wider than this setting
autoShift - when set to true it automatically drags the other handle along when range width reaches maximum; when set to false handle just can't be moved beyond maximum range width
lowMax - sets the lower handle upper boundary so it's impossible to set lower handle beyond this value
topMin - sets the upper handle lower boundary so it's impossible to set upper handle below this value
This is a working example of such range slider.
This is the extra code that has to be run after jQuery slider. It actually rewrites one of its internal functions to also check the new settings. This code will only change slider code when slider script has been loaded (hence the first if statement that checks whether slider widget has been loaded):
(function ($) {
if ($.ui.slider)
{
// add minimum range length option
$.extend($.ui.slider.prototype.options, {
minRangeSize: 0,
maxRangeSize: 100,
autoShift: false,
lowMax: 100,
topMin: 0
});
$.extend($.ui.slider.prototype, {
_slide: function (event, index, newVal) {
var otherVal,
newValues,
allowed,
factor;
if (this.options.values && this.options.values.length)
{
otherVal = this.values(index ? 0 : 1);
factor = index === 0 ? 1 : -1;
if (this.options.values.length === 2 && this.options.range === true)
{
// lower bound max
if (index === 0 && newVal > this.options.lowMax)
{
newVal = this.options.lowMax;
}
// upper bound min
if (index === 1 && newVal < this.options.topMin)
{
newVal = this.options.topMin;
}
// minimum range requirements
if ((otherVal - newVal) * factor < this.options.minRangeSize)
{
newVal = otherVal - this.options.minRangeSize * factor;
}
// maximum range requirements
if ((otherVal - newVal) * factor > this.options.maxRangeSize)
{
if (this.options.autoShift === true)
{
otherVal = newVal + this.options.maxRangeSize * factor;
}
else
{
newVal = otherVal - this.options.maxRangeSize * factor;
}
}
}
if (newVal !== this.values(index))
{
newValues = this.values();
newValues[index] = newVal;
newValues[index ? 0 : 1] = otherVal;
// A slide can be canceled by returning false from the slide callback
allowed = this._trigger("slide", event, {
handle: this.handles[index],
value: newVal,
values: newValues
});
if (allowed !== false)
{
this.values(index, newVal, true);
this.values((index + 1) % 2, otherVal, true);
}
}
} else
{
if (newVal !== this.value())
{
// A slide can be canceled by returning false from the slide callback
allowed = this._trigger("slide", event, {
handle: this.handles[index],
value: newVal
});
if (allowed !== false)
{
this.value(newVal);
}
}
}
}
});
}
})(jQuery);

Resources