Appearance
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];
}