Appearance
Fuzzy search
This example shows how to implement a searchbar to find nodes in the graph.
ts
import Ogma, { NodeId, RawGraph } from '@linkurious/ogma';
type NodeData = {
latin_name: string;
};
const ogma = new Ogma<NodeData>({
container: 'graph-container'
});
// add highlight style rule
ogma.styles.createClass({
name: 'highlighted',
nodeAttributes: {
radius: function (node) {
return Number(node.getAttribute('radius')) * 1.5;
},
halo: {
color: '#cccccc',
width: 20
}
}
});
type SearchResult = {
index: number;
string: string;
original: {
id: string;
data: {
latin_name: string;
};
};
score: number;
};
const highlightNode = function (id: NodeId) {
ogma.getNodes().removeClass('highlighted');
if (id) ogma.getNode(id)!.addClass('highlighted');
};
const selectNode = function (id: NodeId) {
ogma.getSelectedNodes().setSelected(false);
if (id) ogma.getNode(id)!.setSelected(true);
};
const search = {
data: { nodes: [], edges: [] } as RawGraph,
query: (query: string) => {}
};
const init = function (graph: RawGraph) {
const resultsContainer = document.querySelector('#results') as HTMLElement;
let currentItem = 0; // current item selected in the list
const input = document.querySelector('#search-input') as HTMLInputElement;
// store raw data
search.data = graph;
// expose search procedure
search.query = function (value: string) {
// perform query using fuzzy.js
const results = fuzzy.filter(value, search.data.nodes, {
// highlight the matching substring
pre: '<span class="match">',
post: '</span>',
extract: (item: { data: NodeData }) => item.data.latin_name
});
// render the list HTML
resultsContainer.innerHTML =
'<div class="dropdown-menu">' +
results
.map(
(result: SearchResult, i: number) =>
`<div
class="dropdown-item ${i === currentItem ? 'hover' : ''}
data-position="${i}"
data-index="${result.index}"
data-value="${result.original.data.latin_name}"
data-id="${result.original.id}">${result.string}</div>`
)
.join('') +
'</div>';
highlightNode(results[currentItem].original.id);
};
// listen for input
input.addEventListener('keyup', function (evt) {
if (evt.keyCode === 40) {
// down arrow
currentItem++;
} else if (evt.keyCode === 38) {
// up arrow
currentItem--;
} else if (evt.keyCode === 13) {
// enter
const item =
resultsContainer.querySelectorAll('.dropdown-item')[currentItem];
input.value = item.getAttribute('data-value')!;
selectNode(item.getAttribute('data-id')!);
}
// wait for the next frame to throttle the search
requestAnimationFrame(() => {
search.query(input.value);
});
});
// highlight on hover
resultsContainer.addEventListener('mousemove', evt => {
requestAnimationFrame(() => {
const target = evt.target as HTMLElement;
const id = target.getAttribute('data-id')!;
currentItem = Number(target.getAttribute('data-position') || 0);
highlightNode(id);
});
});
// select on click
resultsContainer.addEventListener('click', evt => {
const target = evt.target as HTMLElement;
const id = target.getAttribute('data-id')!;
currentItem = Number(target.getAttribute('data-position') || 0);
input.value = target.getAttribute('data-value')!;
selectNode(id);
});
return graph;
};
Ogma.parse
.jsonFromUrl<NodeData>('paris-metro.json') // load data
.then(init) // init search
.then(graph => ogma.setGraph(graph)) // add graph to ogma
.then(() => ogma.view.locateGraph()) // position
.then(() => {
// trigger up the search
const input = document.querySelector('#search-input')! as HTMLInputElement;
input.value = 'Lou';
search.query('Lou');
input.focus();
});
html
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<script src="https://cdn.jsdelivr.net/npm/fuzzy@0.1.3/lib/fuzzy.js"></script>
<link type="text/css" rel="stylesheet" href="styles.css" />
</head>
<body>
<div id="graph-container"></div>
<div class="toolbar">
<input type="search" placeholder="Search station" id="search-input" />
<div id="results" class="dropdown"></div>
</div>
<script type="module" src="index.ts"></script>
</body>
</html>
css
html,
body {
padding: 0;
margin: 0;
font: normal 18px "Helvetica Neue", Helvetica, Arial, sans-serif;
}
#graph-container {
top: 0;
bottom: 0;
left: 0;
right: 0;
position: absolute;
margin: 0;
overflow: hidden;
}
.toolbar {
display: block;
position: absolute;
top: 20px;
right: 20px;
padding: 10px;
box-shadow: 0 1px 5px rgba(0, 0, 0, 0.65);
border-radius: 4px;
background: #ffffff;
color: #222222;
font-weight: 300;
min-width: 35%;
}
#search-input {
width: 100%;
padding: 1rem;
font-size: 1rem;
}
#search-input:focus {
outline: 0;
}
.match {
font-weight: bold;
text-decoration: underline;
}
.dropdown {
position: relative;
}
.dropdown-item {
display: block;
width: 100%;
padding: 0.25rem 1.5rem;
clear: both;
font-weight: 400;
color: #212529;
text-align: inherit;
white-space: nowrap;
background-color: transparent;
border: 0;
cursor: pointer;
box-sizing: border-box;
}
.dropdown-item:hover,
.dropdown-item.hover {
background: #dddddd;
}
.dropdown-menu {
position: absolute;
top: 100%;
right: 0;
z-index: 1000;
display: block;
float: left;
min-width: 10rem;
padding: 0.5rem 0;
margin: 0.125rem 0 0;
font-size: 1rem;
color: #212529;
text-align: left;
list-style: none;
background-color: #fff;
background-clip: padding-box;
border: 1px solid rgba(0, 0, 0, 0.15);
border-radius: 0.25rem;
max-height: 500px;
overflow-y: auto;
}
json
{
"nodes": [
{
"degree": 5,
"id": "Gare de LyonPARIS-12EME",
"inDegree": 3,
"outDegree": 2,
"active": false,
"halo": false,
"hidden": false,
"latitude": 48.844757,
"longitude": 2.3740723,
"pinned": false,
"size": 42.42640687119285,
"text": "Gare de Lyon",
"x": 525.1153564453125,
"y": -281,
"isNode": true,
"data": {
"latin_name": "Gare de Lyon",
"Eccentricity": 23,
"Betweenness Centrality": 66284.82695915208,
"Closeness Centrality": 8.378731343283581,
"local_name": "Gare de Lyon",
"location": "PARIS-12EME",
"Eigenvector Centrality": 1,
"lines": "Line14-Line1-RERA-RERD",
"latitude": 48.844757,
"longitude": 2.3740723
},
"attributes": {
"radius": 42.42640687119285,
"color": [
"#67328E",
"#F2C931"
],
"text": "Gare de Lyon"
}
},
{
"degree": 3,
"id": "Gare du NordPARIS-10EME",
"inDegree": 2,
"outDegree": 1,
"active": false,
"halo": false,
"hidden": false,
"latitude": 48.880035,
"longitude": 2.3545492,
"pinned": false,
"size": 42.42640687119285,
"text": "Gare du Nord",
"x": 496.4261779785156,
"y": -1209,
"isNode": true,
"data": {
"latin_name": "Gare du Nord",
"Eccentricity": 25,
"Betweenness Centrality": 36901.69440836936,
"Closeness Centrality": 8.89179104477612,
"local_name": "Gare du Nord",
"location": "PARIS-10EME",
"Eigenvector Centrality": 0.5366748142943254,
"lines": "Line4-Line5-RERB-RERD",
"latitude": 48.880035,
"longitude": 2.3545492
},
"attributes": {
"radius": 42.42640687119285,
"color": [
"#BB4D98",
"#DE8B53"
],
"text": "Gare du Nord"
}
},
{
"degree": 7,
"id": "NationPARIS-12EME",
"inDegree": 3,
"outDegree": 4,
"active": false,
"halo": false,
"hidden": false,
"latitude": 48.848465,
"longitude": 2.3959057,
"pinned": false,
"size": 60,
"text": "Nation",
"x": 989.2464599609375,
"y": -235,
"isNode": true,
"data": {
"latin_name": "Nation",
"Eccentricity": 24,
"Betweenness Centrality": 31660.579437229462,
"Closeness Centrality": 9.078358208955224,
"local_name": "Nation",
"location": "PARIS-12EME",
"Eigenvector Centrality": 0.7770134775054275,
"lines": "Line1-Line2-Line6-Line9-RERA",
"latitude": 48.848465,
"longitude": 2.3959057
},
"attributes": {
"radius": 60,
"color": [
"#F2C931",
"#216EB4",
"#75c695",
"#CDC83F"
],
"text": "Nation"
}
},
{
"degree": 6,
"id": "Charles de Gaulle - EtoilePARIS-08EME",
"inDegree": 1,
"outDegree": 5,
"active": false,
"halo": false,
"hidden": false,
"latitude": 48.87441,
"longitude": 2.2957628,
"pinned": false,
"size": 51.96152422706631,
"text": "Charles de Gaulle - Etoile",
"x": -973.6716918945312,
"y": -1286,
"isNode": true,
"data": {
"latin_name": "Charles de Gaulle - Etoile",
"Eccentricity": 26,
"Betweenness Centrality": 30386.45905483406,
"Closeness Centrality": 9.42910447761194,
"local_name": "Charles de Gaulle - Etoile",
"location": "PARIS-08EME",
"Eigenvector Centrality": 0.35464528135158707,
"lines": "Line1-Line2-Line6-RERA",
"latitude": 48.87441,
"longitude": 2.2957628
},
"attributes": {
"radius": 51.96152422706631,
"color": [
"#F2C931",
"#216EB4",
"#75c695"
],
"text": "Charles de Gaulle - Etoile"
}
},
{
"degree": 4,
"id": "InvalidesPARIS-07EME",
"inDegree": 2,
"outDegree": 2,
"active": false,
"halo": false,
"hidden": false,
"latitude": 48.862553,
"longitude": 2.313989,
"pinned": false,
"size": 42.42640687119285,
"text": "Invalides",
"x": -689.1814575195312,
"y": -520,
"isNode": true,
"data": {
"latin_name": "Invalides",
"Eccentricity": 25,
"Betweenness Centrality": 22916.702586302596,
"Closeness Centrality": 9.598880597014926,
"local_name": "Invalides",
"location": "PARIS-07EME",
"Eigenvector Centrality": 0.44784291910876195,
"lines": "Line13-Line8-RERC",
"latitude": 48.862553,
"longitude": 2.313989
},
"attributes": {
"radius": 42.42640687119285,
"color": [
"#89C7D6",
"#C5A3CA"
],
"text": "Invalides"
}
},
{
"degree": 8,
"id": "ChâteletPARIS-01ER",
"inDegree": 0,
"outDegree": 8,
...