Skip to content
  1. Examples

Import progressbar

This example shows how to display a loader while Ogma is parsing and adding the graph to the visualisation.
It uses batchSize option to add the graph by chunks and display a loader between each chunk.

ts
import Ogma from '@linkurious/ogma';
import { Progressbar } from './progressbar';

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();

const response = await fetch('files/github.json');
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();
  });
};

const json = await read();
const graph = await Ogma.parse.json(json!);
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 onReady = () => {
  progressbar.hide();
  ogma.events.off(onProgress);
  const nodes = ogma.getNodes().size;
  const edges = ogma.getEdges().size;
  // show graph size
  document.getElementById('n')!.textContent = `nodes: ${nodes}`;
  document.getElementById('e')!.textContent = `edges: ${edges}`;
};

ogma.events.on(['addNodes', 'addEdges'], onProgress);

// predict the camera location and then add the graph
await ogma.view.locateRawGraph(graph);
await ogma.setGraph(graph, { batchSize: 500 });
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" />
  </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;
}
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;
  }
}