Appearance
Blueprint new
This example shows how to combine graph data with a blueprint or a schematic using the layers API, thus making it interactive. Switch between different views to change the layout and the background of the graph.
ts
import Ogma, { CanvasLayer } from '@linkurious/ogma';
import { loadBlueprint, drawBlueprint } from './render';
import { positions, projection } from './position';
// blueprint view modes
const VIEW_MODES = ['side', 'top'];
let viewMode = VIEW_MODES[0];
let drawLayer: CanvasLayer;
// ogma & style rules
const ogma = new Ogma({
container: 'graph-container',
options: { backgroundColor: '#2555aa' }
});
const labelStyles = {
color: '#fff',
backgroundColor: '#000',
margin: 8,
padding: 4
};
// regular styles
ogma.styles.addRule({
nodeAttributes: {
text: labelStyles
}
});
// hovered styles
ogma.styles.setHoveredNodeAttributes({
text: labelStyles
});
// load the blueprint
Ogma.parse
.jsonFromUrl('elements.json')
.then(graph => ogma.setGraph(graph))
.then(() => loadBlueprint())
.then(image => {
drawLayer = ogma.layers.addCanvasLayer(ctx =>
drawBlueprint(ctx, image, viewMode === 'side')
);
return drawLayer.moveToBottom();
})
.then(() => onViewModeChange())
.then(() => ogma.view.locateGraph())
.then(() => ogma.view.setZoom(0.4));
// handle view mode changes
const form = document.getElementById('wrapper') as HTMLFormElement;
form.addEventListener('change', () => setTimeout(onFormChange));
const onViewModeChange = () => {
// update the annotation layers
drawLayer?.refresh();
const sideView = viewMode === 'side';
const nodes = ogma.getNodes();
// update nodes positions
nodes.setAttributes(
// use the right coordinates from the array depending on the view mode
nodes.map(node => projection(positions[node.getId() as number], sideView))
);
};
const onFormChange = () => {
viewMode =
VIEW_MODES[
Number(
Array.prototype.filter.call(form['layering'], input => {
return input.checked;
})[0].value
)
];
onViewModeChange();
};
html
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<link type="text/css" rel="stylesheet" href="styles.css" />
</head>
<body>
<div id="graph-container"></div>
<form id="wrapper" class="control-bar">
<p>Choose the view:</p>
<label
><input type="radio" name="layering" checked value="0" /> Side</label
>
<label><input type="radio" name="layering" value="1" /> Top</label>
</form>
<script type="module" src="index.ts"></script>
</body>
</html>
css
#graph-container {
top: 0;
bottom: 0;
left: 0;
right: 0;
position: absolute;
margin: 0;
overflow: hidden;
}
#wrapper {
position: absolute;
top: 10px;
right: 10px;
z-index: 400;
background: white;
padding: 10px;
}
.control-bar {
font-family: Helvetica, Arial, sans-serif;
box-shadow: 0 1px 5px rgba(0, 0, 0, 0.65);
border-radius: 4px;
}
.control-bar label {
display: block;
}
.concept {
height: 10px;
width: 10px;
background-color: #2ca02c;
border-radius: 50%;
display: inline-block;
}
.table {
height: 10px;
width: 10px;
background-color: #ff9896;
border-radius: 50%;
display: inline-block;
}
json
{
"nodes": [
{
"id": 0,
"attributes": {
"x": 2995,
"y": 300,
"color": "#2ca02c",
"radius": 50,
"shape": "circle",
"text": "Front Right Wheel"
},
"x": 421.66666666666674,
"y": 0,
"r": 80,
"layer": 0,
"sink": false
},
{
"id": 1,
"attributes": {
"x": 2667.5,
"y": 200,
"color": "#ff7f0e",
"radius": 50,
"shape": "circle",
"text": "Front Left Wheel"
},
"x": 337.33333333333337,
"y": 99.47145877378435,
"r": 80,
"sink": false
},
{
"id": 2,
"attributes": {
"x": 2995,
"y": 300,
"color": "#2ca02c",
"radius": 50,
"shape": "circle",
"text": "Back Right Wheel"
},
"x": 421.66666666666674,
"y": 0,
"r": 80,
"layer": 0,
"sink": false
},
{
"id": 3,
"attributes": {
"x": 2667.5,
"y": 200,
"color": "#ff7f0e",
"radius": 50,
"shape": "circle",
"text": "Back Left Wheel"
},
"x": 337.33333333333337,
"y": 99.47145877378435,
"r": 80,
"sink": false
},
{
"id": 4,
"attributes": {
"x": 3127.5,
"y": 250,
"color": "#1f77b4",
"radius": 50,
"shape": "circle",
"text": "Driver"
},
"x": 506,
"y": 99.47145877378435,
"r": 80,
"sink": false
},
{
"id": 5,
"attributes": {
"x": 3127.5,
"y": 250,
"color": "#1f00b4",
"radius": 50,
"shape": "circle",
"text": "Shovel Top Corner"
},
"x": 506,
"y": 99.47145877378435,
"r": 80,
"sink": false
},
{
"id": 6,
"attributes": {
"x": 3127.5,
"y": 250,
"color": "#1f00b4",
"radius": 50,
"shape": "circle",
"text": "Shovel Bottom Corner"
},
"x": 506,
"y": 99.47145877378435,
"r": 80,
"sink": false
}
],
"edges": []
}
ts
import { Point } from '@linkurious/ogma';
export type Vector<N extends number, T = [number, ...Array<number>]> = T & {
readonly length: N;
};
/**
* Blueprint element positions in the 3D real-world space.
*/
export const positions: Vector<3>[] = [
// wheel positions
[700, 740, 300],
[700, 740, 760],
[1450, 740, 300],
[1450, 740, 760],
// driver position
[1080, 340, 530],
// shovel
[200, 850, 400],
[400, 900, 650]
];
/**
* Project the blueprint element 3D positions into the 2D graph space.
*/
export const projection = ([x, y, z]: Vector<3>, side: boolean): Point =>
side ? { x, y } : { x, y: z };
ts
/**
* Load the original blueprint image.
*/
export const loadBlueprint = (): Promise<HTMLImageElement> =>
new Promise(resolve => {
const image = new Image(3470, 2301); // using optional size for image
image.onload = () => resolve(image); // resolve when image has loaded
image.src = 'img/blueprint.png';
});
/**
* Draw the original blueprint image.
*/
export const drawBlueprint = (
ctx: CanvasRenderingContext2D,
image: HTMLImageElement,
side: boolean
) => {
const dx = 280;
const dy = side ? 200 : 1130;
const dWidth = 1960;
const dHeight = 930;
ctx.drawImage(image, dx, dy, dWidth, dHeight, 0, 0, dWidth, dHeight);
};