Skip to content
  1. Examples

Annotations new

This example shows how you can integrate @linkurious/ogma-annotations plugin with Ogma to create a graph visualization with annotations. You can draw arrows that can be snapped to nodes, and add text annotations to the graph. You can link text boxes to nodes and see how the arrows will follow the nodes and text boxes when they move.

ts
import Ogma from '@linkurious/ogma';

// on the playground, we are using the CDN version of annotation plugin,
// normally, in your app you would import it like this:
import { Control, createArrow, createText } from '@linkurious/ogma-annotations';
import { getAnnotationsBounds } from '@linkurious/ogma-annotations';

const ogma = new Ogma({
  container: 'graph-container'
});

const graph = await Ogma.parse.jsonFromUrl('files/paris.json');
ogma.styles.addNodeRule({
  text: {
    font: 'IBM Plex Sans'
  }
});

await ogma.view.locateRawGraph(graph);
await ogma.setGraph(graph);

const control = new Control(ogma);

// load annotations
const annotations = await fetch('./annotations.json').then(response =>
  response.json()
);

// add them to the visualization
control.add(annotations);

// init the UI
const drawTextButton = document.getElementById(
  'draw-text'
) as HTMLButtonElement;
const drawArrowButton = document.getElementById(
  'draw-arrow'
) as HTMLButtonElement;

// draw text box
drawTextButton.addEventListener('click', evt => {
  const button = evt.target as HTMLButtonElement;

  if (button.classList.contains('active')) return;

  button.classList.toggle('active');
  drawArrowButton.classList.remove('active');
  drawArrowButton.setAttribute('disabled', 'true');

  ogma.events.once('mousedown', evt => {
    const { x, y } = ogma.view.screenToGraphCoordinates(evt);
    const text = createText(x, y, 0, 0, '', {
      fontSize: 34,
      font: 'IBM Plex Sans',
      padding: 22,
      color: '#1499ff',
      strokeWidth: 1,
      strokeColor: 'rgba(0,0,0,0.35)'
    });
    control.startText(x, y, text);
    control.once('dragend', () => {
      drawArrowButton.disabled = false;
      button.classList.remove('active');
    });
  });
});

// draw arrow
drawArrowButton.addEventListener('click', evt => {
  const button = evt.target as HTMLButtonElement;

  if (button.classList.contains('active')) return;

  button.classList.toggle('active');
  drawTextButton.classList.remove('active');
  drawTextButton.setAttribute('disabled', 'true');

  ogma.events.once('mousedown', evt => {
    const { x, y } = ogma.view.screenToGraphCoordinates(evt);
    const arrow = createArrow(x, y, x, y, {
      strokeWidth: 8,
      strokeColor: '#811313',
      strokeType: 'plain',
      head: 'arrow'
    });
    control.startArrow(x, y, arrow);
    control.once('dragend', () => {
      drawTextButton.disabled = false;
      button.classList.remove('active');
    });
  });
});

// stop drawing when pressing escape
ogma.events.on('keyup', evt => {
  if (evt.key === 'esc') {
    control.cancelDrawing();
    drawTextButton.classList.remove('active');
    drawArrowButton.classList.remove('active');
    drawTextButton.disabled = false;
    drawArrowButton.disabled = false;
  }
});

document.getElementById('snapshot')!.addEventListener('click', async () => {
  const view = ogma.view.get();
  const bounds = ogma.view
    .getGraphBoundingBox()
    .extend(getAnnotationsBounds(control.getAnnotations()))
    .pad(30);
  await ogma.view.moveToBounds(bounds, { duration: 0 });
  const zoom = ogma.view.getZoom();
  const width = bounds.width * zoom;
  const height = bounds.height * zoom;

  const desiredWidth = 566;
  const desiredHeight = 420;

  const ratio = Math.min(desiredWidth / width, desiredHeight / height);
  await ogma.view.setZoom(zoom * ratio, { ignoreZoomLimits: true });
  await ogma.export.svg({
    background: '#fff',
    width: desiredWidth,
    height: desiredHeight,
    clip: true
  });
  await ogma.view.set(view);
});

document.getElementById('download')!.addEventListener('click', () => {
  const annotations = control.getAnnotations();
  const data = JSON.stringify(annotations, null, 2);
  const blob = new Blob([data], { type: 'application/json' });
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url;
  a.download = 'annotations.json';
  a.click();
  URL.revokeObjectURL(url);
});
html
<!doctype html>
<html>
  <head>
    <meta charset="utf-8" />

    <link
      rel="stylesheet"
      href="https://cdn.jsdelivr.net/npm/@linkurious/ogma-annotations@1.1.16/dist/style.css"
    />
    <link type="text/css" rel="stylesheet" href="styles.css" />
    <link rel="preconnect" href="https://fonts.googleapis.com" />
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
    <link
      href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;1,100;1,200;1,300;1,400;1,500;1,600;1,700&display=swap"
      rel="stylesheet"
    />
  </head>
  <body>
    <div id="graph-container"></div>
    <div id="controls">
      <button id="draw-arrow" title="Arrow"></button>
      <button id="draw-text" title="Text"></button>
      <button id="snapshot" title="Snapshot"></button>
      <button id="download" title="Download annotations"></button>
    </div>
    <script type="module" src="index.ts"></script>
  </body>
</html>
css
html,
body {
  font-family: 'IBM Plex Sans', Arial, Helvetica, sans-serif;
}

#graph-container {
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
  position: absolute;
  margin: 0;
  overflow: hidden;
}

#controls {
  position: absolute;
  top: 2em;
  right: 2em;
  display: flex;
}

#controls button {
  width: 3em;
  height: 2.5em;
  margin: 0;
  border: 1px solid rgba(0, 0, 0, 0.2);
  background-color: #fff;
  color: #444;
  cursor: pointer;
  background-size: 50%;
  background-repeat: no-repeat;
  background-position: center center;
  margin-left: -1px;
}

#controls button:hover,
#controls button:active,
#controls button.active {
  color: #222;
  background-color: #f0f0f0;
  border-color: rgba(0, 0, 0, 0.4);
}

#controls button:disabled {
  cursor: not-allowed;
  color: #999;
  background-color: #f9f9f9;
  border-color: rgba(0, 0, 0, 0.2);
  opacity: 0.5;
}

#controls button:first-child {
  border-top-left-radius: 5px;
  border-bottom-left-radius: 5px;
  border-right-width: 0;
}

#controls button:last-child {
  border-top-right-radius: 5px;
  border-bottom-right-radius: 5px;
}

#draw-arrow {
  background-image: url('./img/icon-arrow.svg');
}

#draw-text {
  background-image: url('./img/icon-textbox.svg');
}

#download {
  background-image: url('./img/icon-download.svg');
}

#snapshot {
  background-image: url('./img/icon-photo.svg');
}
json
{
  "name": "ogma-embed-ogma-playground",
  "dependencies": {
    "@linkurious/ogma-annotations": "1.1.23"
  }
}
json
{
  "type": "FeatureCollection",
  "features": [
    {
      "type": "Feature",
      "id": 2,
      "properties": {
        "type": "arrow",
        "style": {
          "strokeType": "plain",
          "strokeColor": "#2D00A6",
          "strokeWidth": 4,
          "head": "halo-dot",
          "tail": "none"
        },
        "link": {
          "start": {
            "id": 0,
            "side": "start",
            "type": "text",
            "magnet": {
              "x": 1,
              "y": 0.5
            }
          },
          "end": {
            "id": "0",
            "side": "end",
            "type": "node",
            "magnet": {
              "x": -45.2809297350213,
              "y": -159.10598861297984
            }
          }
        }
      },
      "geometry": {
        "type": "LineString",
        "coordinates": [
          [-663.8988904175328, -356.74430552470324],
          [-45.21970709863166, -159.31319405632294]
        ]
      }
    },
    {
      "id": "_ehl9o_Yw7ubR17UConfe",
      "type": "Feature",
      "properties": {
        "type": "arrow",
        "style": {
          "strokeType": "plain",
          "strokeColor": "#e8346d",
          "strokeWidth": 5,
          "head": "halo-dot",
          "tail": "none"
        },
        "link": {
          "end": {
            "id": "1",
            "side": "end",
            "type": "node",
            "magnet": {
              "x": -503.6878974740729,
              "y": 348.9457782965114
            }
          },
          "start": {
            "id": "yHia09RukDxkVI5Jb50ze",
            "side": "start",
            "type": "text",
            "magnet": {
              "x": 0,
              "y": 0.5
            }
          }
        }
      },
      "geometry": {
        "type": "LineString",
        "coordinates": [
          [447.89908613443674, 561.6440218094451],
          [-503.57114352516686, 351.9211278358034]
        ]
      }
    },
    {
      "id": "rHX86vJ2-0tp6MahHTlGF",
      "type": "Feature",
      "properties": {
        "type": "arrow",
        "style": {
          "strokeType": "plain",
          "strokeColor": "#e8346d",
          "strokeWidth": 5,
          "head": "halo-dot",
          "tail": "none"
        },
        "link": {
          "end": {
            "id": "1317",
            "side": "end",
            "type": "node",
            "magnet": {
              "x": 783.9361210917288,
              "y": -68.3140821722226
            }
          },
          "start": {
            "id": "yHia09RukDxkVI5Jb50ze",
            "side": "start",
            "type": "text",
            "magnet": {
              "x": 0.5,
              "y": 0
            }
          }
        }
      },
      "geometry": {
        "type": "LineString",
        "coordinates": [
          [656.5223749658235, 523.2047849237448],
          [600.8912284444023, -22.274441521509942]
        ]
      }
    },
    {
      "type": "Feature",
      "id": 0,
      "properties": {
        "type": "text",
        "content": "This is a cluster",
        "style": {
          "font": "IBM Plex Sans",
          "fontSize": 52,
          "color": "#2D00A6",
          "background": "#EDE6FF",
          "strokeWidth": 0,
          "borderRadius": 8,
          "padding": 12,
          "strokeType": "plain"
        }
      },
      "geometry": {
        "type": "Polygon",
        "coordinates": [
          [
            [-1055.6712819373658, -400.20513968406317],
            [-663.8988904175328, -400.20513968406317],
            [-663.8988904175328, -313.28347136534336],
            [-1055.6712819373658, -313.28347136534336],
            [-1055.6712819373658, -400.20513968406317]
          ]
        ],
        "bbox": [
          -1055.6712819373658, -400.20513968406317, -663.8988904175328,
          -313.28347136534336
        ]
      }
    },
    {
      "id": "yHia09RukDxkVI5Jb50ze",
      "type": "Feature",
      "properties": {
        "type": "text",
        "content": "This is another one",
        "style": {
          "font": "IBM Plex Sans",
          "fontSize": 32,
          "color": "#e8346d",
          "background": "#FAEDF7",
          "strokeWidth": 0,
          "borderRadius": 8,
          "padding": 12,
          "strokeType": "plain",
          "cornerRadius": 8
        }
      },
      "geometry": {
        "type": "Polygon",
        "coordinates": [
          [
            [447.89908613443674, 523.2047849237448],
            [865.1456637972101, 523.2047849237448],
            [865.1456637972101, 600.0832586951456],
            [447.89908613443674, 600.0832586951456],
            [447.89908613443674, 523.2047849237448]
          ]
        ],
        "bbox": [
          447.89908613443674, 523.2047849237448, 865.1456637972101,
          600.0832586951456
        ]
      }
    }
  ]
}