//
//     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