Skip to content
  1. Examples

PDF

This example uses jsPDF and svg2pdf libraries to create a quality PDF export of Ogma visualisation. It uses SVG export to ensure the quality of the visualisation.

ts
import Ogma from '@linkurious/ogma';
import { getIconCode } from './utils';

const ogma = new Ogma({
  container: 'graph-container'
});

const graph = await Ogma.parse.jsonFromUrl('files/solarCity.json');
await ogma.setGraph(graph);
await ogma.view.locateGraph();
await updatePdf();

// Enable the legend
ogma.tools.legend.enable({
  position: 'bottom',
  titleTextAlign: 'center',
  shapeColor: 'black',
  circleStrokeWidth: 1
});

async function updatePdf() {
  const [svgSrc, legend] = await Promise.all([
    // export to svg
    ogma.export.svg({ download: false, clip: true }),
    ogma.tools.legend.export()
  ]);
  // convaert SVG to DOM element
  const svg = toElement<SVGSVGElement>(svgSrc);
  // use the rendered svg to create a pdf
  const doc = new jspdf.jsPDF({ format: 'a4', unit: 'pt' });

  const svgWidth = parseInt(svg.getAttribute('width') || '0');
  const svgHeight = parseInt(svg.getAttribute('height') || '0');

  const margin = 15;

  const pageWidth = doc.getPageWidth() - margin * 2;
  const pageHeight = doc.getPageHeight() - margin * 2;

  // vertical cursor
  let y = margin;

  // fitting ratio canvas to page
  let ratio = 1 / Math.max(svgWidth / pageWidth, svgHeight / pageHeight);

  // seems like big sizes breaks it
  const width = svgWidth * ratio;
  const height = svgHeight * ratio;

  // resize SVG
  svg.setAttribute('width', width.toString());
  svg.setAttribute('height', height.toString());

  // fit the contents of SVG
  svg.setAttribute('viewBox', `0 0 ${width / ratio} ${height / ratio}`);

  // render SVG to PDF
  await doc.svg(svg, { x: margin, y, width, height });
  // scale the legend to fit the vis
  ratio = 1 / (legend.width / width);
  const legendWidth = legend.width * ratio;
  const legendHeight = legend.height * ratio;
  doc.addImage(
    legend,
    'PNG',
    margin,
    y + margin + height - legendHeight,
    legendWidth,
    legendHeight
  );

  // write caption here, not to mangle the visualisation fonts
  doc.setFontSize(12);
  doc.setFont('Helvetica', 'bolditalic');
  doc.text('Figure 1:', margin, margin + 8);
  doc.setFont('Helvetica', 'italic');
  doc.text('Visualisation caption', margin + 54, margin + 8);

  // add border
  doc.rect(
    margin,
    y + margin + height - legendHeight,
    legendWidth,
    legendHeight,
    'S'
  );
  // update the preview
  const blobURL = URL.createObjectURL(
    doc.output('blob', { filename: 'ogma.pdf' })
  );
  document.getElementById('pdf-iframe')!.setAttribute('src', blobURL);
}

// Define the Node style rules
ogma.styles.addNodeRule({
  // the label is the value os the property name.
  text: node => node.getData('properties.name'),

  // the size is proportional to the funding_total property
  radius: ogma.rules.slices({
    field: 'properties.funding_total',
    values: { nbSlices: 7, min: 3, max: 10 }
  }),

  // the color is based on the funding_total property (from red to purple)
  color: ogma.rules.slices({
    field: 'properties.funding_total',
    values: [
      '#161344',
      '#3f1c4c',
      '#632654',
      '#86315b',
      '#a93c63',
      '#cd476a',
      '#f35371'
    ]
  }),

  // assign icons based on the node category
  icon: {
    content: ogma.rules.map({
      field: 'categories',
      values: {
        COMPANY: getIconCode('icon-rocket'),
        INVESTOR: getIconCode('icon-landmark'),
        MARKET: getIconCode('icon-gem'),
        PERSON: getIconCode('icon-user-round')
      }
    }),
    font: 'Lucide',
    color: 'white',
    style: 'bold'
  }
});

// Define the Edge style rules
ogma.styles.addEdgeRule({
  // the label is the value os the property name.
  text: edge => edge.getData('type'),

  // the size is proportional to the raised_amount_usd property
  width: ogma.rules.slices({
    field: 'properties.raised_amount_usd',
    values: { nbSlices: 3, min: 1, max: 5 }
  }),

  // the  color is based on the raised_amount_usd property (from blue to black)
  color: ogma.rules.slices({
    field: 'properties.raised_amount_usd',
    values: ['#54aef3', '#326896', '#132b43'],
    reverse: true
  }),

  // the shape is based on the type of edge
  shape: ogma.rules.map({
    field: 'type',
    values: {
      INVESTED_IN: 'arrow',
      ACQUIRED: 'tapered',
      HAS_MARKET: 'dotted'
    }
  })
});

function toElement<T = HTMLElement>(html: string): T {
  const container = document.createElement('div');
  container.innerHTML = html;
  return container.firstElementChild as T;
}
html
<!doctype html>
<html>
  <head>
    <meta charset="utf-8" />

    <link type="text/css" rel="stylesheet" href="styles.css" />
    <link
      href="https://cdn.jsdelivr.net/npm/lucide-static@0.483.0/font/lucide.css"
      rel="stylesheet"
    />
    <link
      href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;1,100;1,200;1,300;1,400;1,500;1,600;1,700&display=swap"
      rel="stylesheet"
    />
    <script src="https://cdn.jsdelivr.net/npm/jspdf@2.3.1/dist/jspdf.umd.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/svg2pdf.js@2.1.0/dist/svg2pdf.umd.js"></script>
  </head>

  <body>
    <div id="graph-container"></div>
    <hr />
    <script type="module" src="index.ts"></script>
    <iframe id="pdf-iframe"></iframe>
  </body>
</html>
css
#graph-container {
  height: 300px;
  width: 100%;
}

#pdf-iframe{
  width: 100%;
  height: 300px;
}
ts
// dummy icon element to retrieve the HEX code, it should be hidden
const placeholder = document.createElement('span');
document.body.appendChild(placeholder);
placeholder.style.visibility = 'hidden';

// helper routine to get the icon HEX code
export function getIconCode(className: string) {
  placeholder.className = className;
  const code = getComputedStyle(placeholder, ':before').content;
  return code[1];
}