Skip to content
  1. Examples

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