//
// Copyright © 2011-2025 Cambridge Intelligence Limited.
// All rights reserved.
//
// Sample Code
//! Export your charts in highly customised PDF reports.
import KeyLines from "keylines";
import { data, defaultStyle, lorem, initZoomIds } from "./custompdf-data.js";
const margin = 0.4 * 72; // 72dpi so this is 0.4inches
const iconFonts = {
"Material Icons": { src: "/fonts/MaterialIcons/MaterialIcons-Regular.ttf" },
};
const ralewayFontSrcs = [
{ name: "Raleway-Bold", src: "/fonts/Raleway/Raleway-Bold.ttf" },
{ name: "Raleway-Regular", src: "/fonts/Raleway/Raleway-Regular.ttf" },
{ name: "Raleway-Italic", src: "/fonts/Raleway/Raleway-Italic.ttf" },
];
const timesNewFont = {
regular: "Times-Roman",
bold: "Times-Bold",
italic: "Times-Italic",
};
const helveticaFont = {
regular: "Helvetica",
bold: "Helvetica-Bold",
italic: "Helvetica-Oblique",
};
const ralewayFont = {
regular: "Raleway-Regular",
bold: "Raleway-Bold",
italic: "Raleway-Italic",
};
let chart;
function downloadPdf(pdfUrl) {
// Create the link to download the image
const snapshotLink = document.createElement("a");
snapshotLink.download = `chart-export.pdf`;
snapshotLink.href = pdfUrl;
snapshotLink.click();
// Important - remember to revoke the url afterwards to free it from browser memory
URL.revokeObjectURL(pdfUrl);
}
async function addExportedChartAndCaption(
width,
height,
fontFamily,
captionText,
doc
) {
doc.font(fontFamily.italic, 10);
const captionHeight = doc.heightOfString(captionText);
// add chart
await chart.export({
type: "pdf",
doc,
fonts: iconFonts,
extents: "view",
fitTo: {
width,
height: height - captionHeight,
},
});
// add caption
doc.moveDown().text(captionText, { align: "center", width });
}
async function loadAndRegisterFonts(fontSrcs, doc) {
let error = false;
for (let i = 0; i < fontSrcs.length; i++) {
const font = fontSrcs[i];
const response = await fetch(font.src);
if (!response.ok) {
console.warn(`Unable to load font ${font.name}`);
error = true;
}
const fontBuffer = await response.arrayBuffer();
doc.registerFont(font.name, fontBuffer);
}
return !error;
}
async function generateLandscapeReport(doc, docTitle, captionText) {
const fontsLoaded = await loadAndRegisterFonts(ralewayFontSrcs, doc);
const fontFamily = fontsLoaded ? ralewayFont : helveticaFont;
// Add title
doc.font(fontFamily.bold, 35).text(docTitle, { align: "center" });
// Allowing a margin separation between them,
// calculate the remaining space for the chart & bullet points
const chartWidth = ((doc.page.width - 3 * margin) * 2) / 3;
const contentHeight = doc.page.height - margin - doc.y;
const chartHeight = (contentHeight * 4) / 5;
// Split the padding equally above and below the main content
doc.y += (contentHeight - chartHeight) / 2;
const contentTop = doc.y;
await addExportedChartAndCaption(
chartWidth,
chartHeight,
fontFamily,
captionText,
doc
);
// Add the bullet points to the right of the chart
doc.x += chartWidth + margin;
doc.y = contentTop;
doc.font(fontFamily.regular, 16);
for (let i = 0; i < 4; i++) {
doc.list([lorem[4]]).moveDown();
}
}
async function generatePortraitReport(doc, docTitle, captionText) {
const fontFamily = timesNewFont;
// Add title
doc.font(fontFamily.bold, 20).text(docTitle, { align: "center" });
const contentWidth = doc.page.width - 2 * margin;
const chartHeight = (doc.page.height - margin - doc.y) * 0.5;
await addExportedChartAndCaption(
contentWidth,
chartHeight,
fontFamily,
captionText,
doc
);
doc.moveDown(2);
// Add columns of text
doc.font(fontFamily.regular, 13);
doc.text(lorem[3], { align: "justify", columns: 2 });
}
async function exportReport() {
const orientation = document.querySelector(
'input[name="orientation"]:checked'
).value;
const docTitle = document.getElementById("title").value;
const captionText = document.getElementById("caption").value;
// Create the PDF documnet
const doc = new PDFDocument({ layout: orientation, margin });
const reportGenerator =
orientation === "landscape"
? generateLandscapeReport
: generatePortraitReport;
await reportGenerator(doc, docTitle, captionText);
const docData = [];
doc.on("data", docData.push.bind(docData));
doc.on("end", () => {
downloadPdf(
URL.createObjectURL(new Blob(docData, { type: "application/pdf" }))
);
});
doc.end();
}
async function startKeyLines() {
const options = {
backColour: "#F0F8FF",
selectedNode: { b: "#111", bw: 5, fbc: "#333", fc: "white" },
iconFontFamily: "Material Icons",
imageAlignment: defaultStyle.imageAlignment,
handMode: true,
logo: { u: "/images/Logo.png" },
};
chart = await KeyLines.create({ container: "klchart", options });
data.items.forEach((item) => {
if (item.type === "node") {
const kind = item.d.kind;
const icon = defaultStyle.kindIcons[kind];
item.fi = {
t: icon,
c: defaultStyle.nodeColours[kind],
};
}
});
await chart.load(data);
await chart.layout("organic", { fit: false });
chart.zoom("fit", { animate: true, time: 500, ids: initZoomIds });
document.getElementById("exportDoc").addEventListener("click", exportReport);
}
async function loadFontsAndStart() {
// wait for Material icons to load before starting keylines
document.fonts.load("24px 'Material Icons'").then(startKeyLines);
}
window.addEventListener("DOMContentLoaded", loadFontsAndStart);
<!DOCTYPE html>
<html lang="en" style="background-color: #2d383f;">
<head>
<meta charset="UTF-8">
<title>PDF Reports</title>
<link rel="stylesheet" type="text/css" href="/css/keylines.css">
<link rel="stylesheet" type="text/css" href="/css/minimalsdk.css">
<link rel="stylesheet" type="text/css" href="/css/sdk-layout.css">
<link rel="stylesheet" type="text/css" href="/css/demo.css">
<link rel="stylesheet" type="text/css" href="/custompdf.css">
<link rel="stylesheet" type="text/css" href="https://fonts.googleapis.com/icon?family=Material+Icons">
<script src="/vendor/jquery.js" defer type="text/javascript"></script>
<script id="keylines" src="/public/keylines.umd.cjs" defer type="text/javascript"></script>
<script src="/vendor/pdfkit.standalone.js" defer type="text/javascript"></script>
<script src="/vendor/svg-to-pdfkit.js" defer type="text/javascript"></script>
<script src="/custompdf.js" crossorigin="use-credentials" type="module"></script>
</head>
<body>
<div class="chart-wrapper demo-cols">
<div class="tab-content-panel flex1" id="lhs" data-tab-group="rhs">
<div class="toggle-content is-visible" id="lhsVisual" style="width:100%; height: 100%;">
<div class="klchart" id="klchart">
</div>
</div>
</div>
<div class="rhs citext closed" id="demorhscontent">
<div class="title-bar">
<svg viewBox="0 0 360 60" style="max-width: 360px; max-height: 60px; font-size: 24px; font-family: Raleway; flex: 1;">
<text x="0" y="38" style="width: 100%;">PDF Reports</text>
</svg>
</div>
<div class="tab-content-panel" data-tab-group="rhs">
<div class="toggle-content is-visible tabcontent" id="controlsTab">
<p>Position the chart as desired, then export it as a high resolution image into a PDF report.</p>
<form autocomplete="off" onsubmit="return false" id="rhsForm">
<div class="cicontent">
<h4 style="padding-top:10px;">Report template</h4>
<label class="radio inline">
<input type="radio" name="orientation" value="portrait" style="opacity:0;" checked id="reportPortrait">
<svg width="90" height="90" viewBox="0 0 254 254" xmlns="https://www.w3.org/2000/svg">
<g transform="matrix(.7882 0 0 .7882 43.634 16.84)" stroke="#333">
<rect width="227.118" height="276.425" x="-.885" y="-1.457" ry="0" transform="matrix(.9382 0 0 1 2.275 2.953)" fill="#fff" stroke-width="2.732"></rect>
<path d="M38.907 21.684h138.879" fill="none" stroke-width="7.695"></path>
<g transform="translate(-5.82 -.49)" fill="none" stroke-width="1.587">
<circle cx="74.326" cy="49.435" r="10.134"></circle>
<circle cx="107.659" cy="86.68" r="10.134"></circle>
<circle cx="76.962" cy="125.051" r="10.134"></circle>
<circle cx="157.114" cy="87.107" r="10.134"></circle>
<path d="M80.324 58.103l20.118 21.92M101.4 94.846L83.76 117.17M117.743 87.226l28.915.464" stroke-width="1.465"></path>
</g>
<g fill="#333" stroke-width="2.908">
<path d="M15.162 151.11h91.604M15.162 180.392h91.604M15.162 219.435h91.604M15.162 209.674h91.604M15.162 229.196h91.604M15.162 248.717h91.604M15.162 238.956h91.604M15.162 258.478h91.604M15.162 160.87h91.604M15.162 190.152h91.604M15.162 170.63h91.604M15.162 199.913h91.604" transform="matrix(.9382 0 0 1 2.275 2.953)"></path>
<path d="M15.162 151.11h91.604M15.162 180.392h91.604M15.162 219.435h91.604M15.162 209.674h91.604M15.162 229.196h91.604M15.162 248.717h91.604M15.162 238.956h91.604M15.162 258.478h91.604M15.162 160.87h91.604M15.162 190.152h91.604M15.162 170.63h91.604M15.162 199.913h91.604" transform="matrix(.9382 0 0 1 99.414 2.953)"></path>
</g>
</g>
</svg>
</label>
<label class="radio inline">
<input type="radio" name="orientation" value="landscape" style="opacity:0;" id="reportLandscape">
<svg width="90" height="90" viewBox="0 0 254 254" xmlns="https://www.w3.org/2000/svg">
<g stroke="#333">
<rect width="294.776" height="212.949" x="-.885" y="-1.457" ry="0" fill="#fff" stroke-width="2.732" transform="matrix(.74636 0 0 .79553 17.99 43.724)"></rect>
<path d="M70.836 58.272h109.465" fill="none" stroke-width="6.0651207199999995"></path>
<g transform="matrix(.79553 0 0 .79553 -1.755 57.632)" fill="none" stroke-width="1.587">
<circle cx="74.326" cy="49.435" r="10.134"></circle>
<circle cx="107.659" cy="86.68" r="10.134"></circle>
<circle cx="76.962" cy="125.051" r="10.134"></circle>
<circle cx="157.114" cy="87.107" r="10.134"></circle>
<path d="M80.324 58.103l20.118 21.92M101.4 94.846L83.76 117.17M117.743 87.226l28.915.464" stroke-width="1.465"></path>
</g>
<g fill="#333" stroke-width="2.24021248">
<path d="M154.765 84.25h68.37M154.765 96.452h68.37M154.765 90.351h68.37M154.765 102.553h68.37"></path>
<path d="M154.765 119.622h68.37M154.765 131.824h68.37M154.765 125.723h68.37M154.765 137.925h68.37"></path>
<path d="M154.765 154.994h68.37M154.765 167.196h68.37M154.765 161.095h68.37M154.765 173.297h68.37"></path>
</g>
</g>
</svg>
</label>
<h4>Page title</h4>
<input id="title" type="text" style="height:30px;" value="Investigative report">
<h4>Figure caption</h4>
<textarea id="caption" type="text" cols="40" rows="4">Policy 910 has two claims, each with an unusually high number of damage reports</textarea>
<input class="btn" type="button" value="Export" style="width:100%" id="exportDoc">
</div>
</form>
</div>
</div>
</div>
</div>
<div id="moreParent">
<div id="moreContainer">
</div>
</div>
<div id="lazyScripts">
</div>
</body>
</html>
#caption {
border: 1px solid rgba(179, 179, 179, 0.705);
padding-left:7px;
padding-top:8px;
resize:none;
}
[type=radio]+ svg {
background-color: rgba(179, 179, 179, 0.521);
border: 1px solid rgba(179, 179, 179, 0.705);
margin-left: -18px;
margin-right: 10px;
}
[type=text] {
font-family: muli;
width:calc(100% - 6px);
margin: 8px 3px 10px 3px;
}
[type=radio]:checked + svg {
border: 2px solid #009968;
}
[type=radio]:hover + svg {
border: 2px solid #00c980;
}
Loading source
