Appearance
Filtering performance new
This example shows how to use the node filter tranfromation in combination with a range slider on bigger datasets.
ts
import Ogma from '@linkurious/ogma';
import { Progressbar } from './progressbar';
import { Slider } from './slider';
const ogma = new Ogma({
container: 'graph-container'
});
ogma.setOptions({
detect: {
// disable the detection of edges and texts, making them impossible to hover or select.
// this improves performances for large graphs
edges: false,
nodeTexts: false,
edgeTexts: false
},
interactions: {
drag: {
enabled: false // disable node dragging
}
}
});
// create progressbar UI
const progressbar = new Progressbar();
fetch('files/ccnr-universe-fa2-ncolorModularity-nsizeBeteewnessCentrality.json')
.then(response => {
const reader = response.body!.getReader();
// Step 2: get total length
const contentLength = +response!.headers.get('Content-Length')!;
// Step 3: read the data
let receivedLength = 0; // received that many bytes at the moment
const chunks: Uint8Array[] = []; // array of received binary chunks (comprises the body)
progressbar.setText('Loading...').show();
const read = (): Promise<string | undefined> => {
return reader.read().then(({ done, value }) => {
if (done) {
progressbar.hide();
const buffer = new Uint8Array(receivedLength);
chunks.reduce((position, chunk) => {
buffer.set(chunk, position); // (4.2)
return position + chunk.length;
}, 0);
// Step 5: decode into a string
const result = new TextDecoder('utf-8').decode(buffer);
return result;
} else {
chunks.push(value!);
receivedLength += value!.length;
progressbar.setValue((receivedLength / contentLength) * 100);
}
if (!done) return read();
});
};
return read();
})
.then(json => Ogma.parse.json(json!))
.then(graph => {
progressbar.setText('Rendering...').setValue(0).show();
const totalSize = graph.nodes.length + graph.edges.length;
// add progress listener
const onProgress = () => {
const currentSize = ogma.getNodes().size + ogma.getEdges().size;
progressbar.setValue((currentSize / totalSize) * 100);
if (currentSize === totalSize) onReady();
};
const updateCounters = (N: number, E: number) => {
document.getElementById('n')!.textContent = `nodes: ${N}`;
document.getElementById('e')!.textContent = `edges: ${E}`;
};
const onReady = () => {
progressbar.hide();
ogma.events.off(onProgress);
const nodes = ogma.getNodes().size;
const edges = ogma.getEdges().size;
// show graph size
updateCounters(nodes, edges);
};
ogma.events.on(['addNodes', 'addEdges'], onProgress);
let minDegree = 0;
let maxDegree = Infinity;
const nodeFilter = ogma.transformations.addNodeFilter({
criteria: node => {
const degree = node.getDegree();
return degree >= minDegree && degree <= maxDegree;
}
});
let timeout = 0;
// predict the camera location and then add the graph
return ogma.view
.locateRawGraph(graph)
.then(() => ogma.setGraph(graph, { batchSize: 4000 }))
.then(() => {
const degrees = ogma.getNodes().getDegree();
const slider = new Slider({
min: Math.min(...degrees),
max: Math.max(...degrees),
onInput: (min, max) => {
clearTimeout(timeout);
timeout = setTimeout(() => {
minDegree = min;
maxDegree = max;
nodeFilter
.refresh()
.then(() =>
updateCounters(ogma.getNodes().size, ogma.getEdges().size)
);
}, 100);
}
});
document.body.appendChild(slider.getContainer());
});
});
html
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<link
href="https://fonts.googleapis.com/css?family=Roboto:400,700"
rel="stylesheet"
type="text/css"
/>
<link type="text/css" rel="stylesheet" href="styles.css" />
<link type="text/css" rel="stylesheet" href="progressbar.css" />
<link type="text/css" rel="stylesheet" href="slider.css" />
</head>
<body>
<div id="graph-container"></div>
<div id="n" class="info n">
loading a large graph, it can take a few seconds...
</div>
<div id="e" class="info e"></div>
<script type="module" src="index.ts"></script>
</body>
</html>
css
body {
margin: 0;
font-family: 'Roboto', sans-serif;
}
#graph-container {
top: 0;
bottom: 0;
left: 0;
right: 0;
position: absolute;
margin: 0;
overflow: hidden;
}
.info {
position: absolute;
color: #fff;
background: #141229;
font-size: 12px;
font-family: monospace;
padding: 5px;
}
.info.n {
top: 0;
left: 0;
}
.info.e {
top: 20px;
left: 0;
}
.slider {
z-index: 1000;
position: absolute;
bottom: 50px;
width: 100%;
padding: 0 50px;
left: 0px;
box-sizing: border-box;
}
.slider [slider] > div > [sign] {
opacity: 1;
}
.slider [slider] > div > [range],
.slider [slider] > div > [sign] {
background-color: #141229;
}
.slider [slider] > div > [sign]:after {
border-top-color: #141229;
}
css
.progressbar {
position: absolute;
top: 50%;
left: 50%;
width: 200px;
height: 200px;
margin-top: -100px;
margin-left: -100px;
}
.progressbar--card {
position: relative;
background: rgba(255, 255, 255, 0.5);
border-radius: 10px;
padding: 10px;
text-align: center;
overflow: hidden;
}
.progressbar--box {
display: inline-block;
}
.progressbar--percent {
position: relative;
width: 150px;
height: 150px;
border-radius: 50%;
box-shadow: inset 0 0 50px #000;
background: #222;
z-index: 1000;
}
.progressbar--num {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
border-radius: 50%;
}
.progressbar--num h2 {
color: #777;
font-weight: 700;
font-size: 30px;
transition: 0.5s;
}
.progressbar--num h2 span {
color: #777;
font-size: 24px;
transition: 0.5s;
}
.progressbar--text {
position: relative;
color: #777;
margin-top: 20px;
font-weight: 700;
font-size: 18px;
letter-spacing: 1px;
text-transform: uppercase;
transition: 0.5s;
}
.progressbar--indicator {
position: relative;
width: 150px;
height: 150px;
z-index: 1000;
}
.progressbar--circle {
width: 100%;
height: 100%;
fill: none;
stroke: #191919;
stroke-width: 10;
stroke-linecap: round;
transform: translate(5px, 145px) rotate(-90deg);
}
.progressbar--progress {
stroke-dasharray: 440;
stroke-dashoffset: 440;
stroke: #ea5565;
}
ts
interface ProgressbarOptions {
/** Gauge radius */
radius?: number;
/** Text to display. Default "progress" */
text?: string;
/** CSS class name. Default "progressbar" */
className?: string;
}
export class Progressbar {
private _container: HTMLElement;
private _text: HTMLElement;
private _num: HTMLElement;
private _progress: SVGCircleElement;
private _circumference: number;
/**
* @param {object} options
* @param {number} options.radius=70 Gauge radius
* @param {string} options.text="Progress" Text to display
* @param {string} className="progressbar"
*/
constructor({
radius = 70,
text = 'Progress',
className = 'progressbar'
}: ProgressbarOptions = {}) {
this._container = this._renderTemplate({ text, radius, name: className });
this._text = this._container.querySelector(`.${className}--text`)!;
this._num = this._container.querySelector(`.${className}--num`)!;
this._progress = this._container.querySelector(`.${className}--progress`)!;
this._circumference = radius * 2 * Math.PI;
}
/**
* Show the progressbar above all other UI
*/
show() {
document.body.appendChild(this._container);
return this;
}
/**
* Remove the progressbar from the DOM
*/
hide() {
document.body.removeChild(this._container);
return this;
}
/**
* @param percent Number between 0 and 100
*/
setValue(percent: number) {
if (percent >= 0 && percent <= 100) {
const c = this._circumference;
const value = c - (c * percent) / 100;
this._progress.style.strokeDashoffset = value.toString();
this._num.innerHTML = `<h2>${percent.toFixed(2)}<span>%</span></h2>`;
}
return this;
}
/**
* @param text
* @returns
*/
setText(text: string) {
this._text.innerText = text;
return this;
}
private _renderTemplate({
text,
radius,
name
}: {
text: string;
radius: number;
name: string;
}) {
const container = document.createElement('div');
container.className = name;
container.innerHTML = `
<div class="${name}--card">
<div class="${name}--box">
<div class="${name}--percent">
<svg class="${name}--indicator">
<circle
class="${name}--circle"
cx="${radius}"
cy="${radius}"
r="${radius}" />
<circle
class="${name}--circle ${name}--progress"
cx="${radius}"
cy="${radius}"
r="${radius}" />
</svg>
<div class="${name}--num">
<h2>0<span>%</span></h2>
</div>
</div>
<h2 class="${name}--text">${text}</h2>
</div>
</div>
`;
return container;
}
}
css
[slider] {
position: relative;
height: 14px;
border-radius: 10px;
text-align: left;
margin: 45px 0 10px 0;
}
[slider] > div {
position: absolute;
left: 13px;
right: 15px;
height: 14px;
}
[slider] > div > [inverse-left] {
position: absolute;
left: 0;
height: 14px;
border-radius: 10px;
background-color: #ccc;
margin: 0 7px;
}
[slider] > div > [inverse-right] {
position: absolute;
right: 0;
height: 14px;
border-radius: 10px;
background-color: #ccc;
margin: 0 7px;
}
[slider] > div > [range] {
position: absolute;
left: 0;
height: 14px;
border-radius: 14px;
background-color: #1abc9c;
}
[slider] > div > [thumb] {
position: absolute;
top: -7px;
z-index: 2;
height: 28px;
width: 28px;
text-align: left;
margin-left: -11px;
cursor: pointer;
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.4);
background-color: #fff;
border-radius: 50%;
outline: none;
}
[slider] > input[type='range'] {
position: absolute;
pointer-events: none;
-webkit-appearance: none;
z-index: 3;
height: 14px;
top: -2px;
width: 100%;
opacity: 0;
}
div[slider] > input[type='range']::-ms-track {
-webkit-appearance: none;
background: transparent;
color: transparent;
}
div[slider] > input[type='range']::-moz-range-track {
-moz-appearance: none;
background: transparent;
color: transparent;
}
div[slider] > input[type='range']:focus::-webkit-slider-runnable-track {
background: transparent;
border: transparent;
}
div[slider] > input[type='range']:focus {
outline: none;
}
div[slider] > input[type='range']::-ms-thumb {
pointer-events: all;
width: 28px;
height: 28px;
border-radius: 0px;
border: 0 none;
background: red;
}
div[slider] > input[type='range']::-moz-range-thumb {
pointer-events: all;
width: 28px;
height: 28px;
border-radius: 0px;
border: 0 none;
background: red;
}
div[slider] > input[type='range']::-webkit-slider-thumb {
pointer-events: all;
width: 28px;
height: 28px;
border-radius: 0px;
border: 0 none;
background: red;
-webkit-appearance: none;
}
div[slider] > input[type='range']::-ms-fill-lower {
background: transparent;
border: 0 none;
}
div[slider] > input[type='range']::-ms-fill-upper {
background: transparent;
border: 0 none;
}
div[slider] > input[type='range']::-ms-tooltip {
display: none;
}
[slider] > div > [sign] {
opacity: 0;
position: absolute;
margin-left: -11px;
top: -39px;
z-index: 3;
background-color: #1abc9c;
color: #fff;
width: 28px;
height: 28px;
border-radius: 28px;
-webkit-border-radius: 28px;
align-items: center;
-webkit-justify-content: center;
justify-content: center;
text-align: center;
}
[slider] > div > [sign]:after {
position: absolute;
content: '';
left: 0;
border-radius: 16px;
top: 19px;
border-left: 14px solid transparent;
border-right: 14px solid transparent;
border-top-width: 16px;
border-top-style: solid;
border-top-color: #1abc9c;
}
[slider] > div > [sign] > span {
font-size: 12px;
font-weight: 700;
line-height: 28px;
}
[slider]:hover > div > [sign] {
opacity: 1;
}
ts
interface SliderOptions {
/** CSS class name. Default "slider" */
className?: string;
/** Initial value. Default 0 */
minValue?: number;
/** Maximum value. Default max */
maxValue?: number;
/** Minimum value. Default 0 */
min?: number;
/** Maximum value. Default 100 */
max?: number;
/** Step value. Default 1 */
step?: number;
/** Callback function to call on input */
onInput?: (min: number, max: number) => void;
}
export class Slider {
private _container: HTMLElement;
private _onInput: (min: number, max: number) => void = () => {};
private _minInput: HTMLInputElement;
private _maxInput: HTMLInputElement;
private _min: number;
private _max: number;
private _minValue: number;
private _maxValue: number;
private _controls: HTMLElement;
constructor({
max = 100,
min = 0,
minValue,
maxValue,
step = 1,
className = 'slider',
onInput = () => {}
}: SliderOptions) {
if (minValue === undefined) minValue = min;
if (maxValue === undefined) maxValue = max;
this._container = this._renderTemplate({
min,
max,
minValue,
maxValue,
className,
step
});
this._controls = this._container.querySelector('[controls]')!;
const inputs = this._container.querySelectorAll<HTMLInputElement>('input')!;
this._minInput = inputs[0];
this._maxInput = inputs[1];
this._max = max;
this._min = min;
this._minValue = minValue;
this._maxValue = maxValue;
this._onInput = onInput;
this._minInput.addEventListener('input', this.onMinChange);
this._maxInput.addEventListener('input', this.onMaxChange);
this._update();
}
private _renderTemplate({
min,
max,
minValue,
maxValue,
step,
className
}: Pick<
Required<SliderOptions>,
'min' | 'max' | 'maxValue' | 'minValue' | 'step' | 'className'
>) {
const container = document.createElement('div');
container.className = className || '';
container.innerHTML = `
<div slider>
<div controls class="elements">
${this._updateControls({ min, max, minValue, maxValue })}
</div>
<input type="range" tabindex="0" value="${minValue}" max="${max}" min="${min}" step="${step}" />
<input type="range" tabindex="0" value="${maxValue}" max="${max}" min="${min}" step="${step}" />
</div>
`;
return container;
}
private _update() {
this._onInput(this._minValue, this._maxValue);
}
private _updateControls({
min,
max,
minValue,
maxValue
}: Pick<
Required<SliderOptions>,
'min' | 'max' | 'minValue' | 'maxValue'
>): string {
const leftPos = (100 / (max - min)) * minValue - (100 / (max - min)) * min;
const rightPos = (100 / (max - min)) * maxValue - (100 / (max - min)) * min;
const rangeMax = 100 - rightPos;
return `
<div inverse-left style="width: ${leftPos}%;"></div>
<div inverse-right style="width: ${rangeMax}%;"></div>
<div range style="left:${leftPos}%; right:${rangeMax}%;"></div>
<span thumb style="left:${leftPos}%;"></span>
<span thumb style="left:${rightPos}%;"></span>
<div sign style="left:${leftPos}%;">
<span id="value">${minValue}</span>
</div>
<div sign style="left:${rightPos}%;">
<span id="value">${maxValue}</span>
</div>
`;
}
private onMinChange = () => {
const value = Math.min(+this._minInput.value, this._max - 1);
this._minInput.value = value.toString();
this._minValue = value;
this._controls.innerHTML = this._updateControls({
min: this._min,
max: this._max,
minValue: value,
maxValue: this._maxValue
});
this._update();
};
private onMaxChange = () => {
const value = Math.max(+this._maxInput.value, this._min + 1);
this._maxInput.value = value.toString();
this._maxValue = value;
this._controls.innerHTML = this._updateControls({
min: this._min,
max: this._max,
minValue: this._minValue,
maxValue: value
});
this._update();
};
public getContainer() {
return this._container;
}
}