Skip to content
  1. Examples
  2. Node styles

All styles

This example provides a tweak panel to edit all NodeAttributes and EdgeAttributes availiable in Ogma. So you can see what they do and how they affect the style of your graph.

ts
import Ogma, {
  Badge,
  Theme,
  NodeAttributes,
  Icon,
  NodeImage
} from '@linkurious/ogma';
import { theme, ICONS, SHAPES } from './theme';
import { ThemeWithSwitches } from './types';
import { GUI } from '@linkurious/ogma-ui-kit/gui';

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

// dummy icon element to retrieve the HEX code
const placeholder = document.getElementById('icon-placeholder')!;
placeholder.style.visibility = 'hidden';

const graph = await ogma.generate.barabasiAlbert({
  nodes: 20,
  scale: 40
});
await ogma.setGraph(graph);
await ogma.layouts.force({ locate: true, duration: 0 });

ogma.styles.setTheme(cleanup(theme));

const gui = new GUI({ title: 'Attributes' });
const nodeAttributesFolder = gui.addFolder('Node Attributes');
nodeAttributesFolder.addColor(theme.nodeAttributes, 'color');
nodeAttributesFolder.add(theme.nodeAttributes, 'radius', 1, 100).step(1);
nodeAttributesFolder.add(theme.nodeAttributes!, 'shape', SHAPES);
nodeAttributesFolder.add(theme.nodeAttributes, 'opacity', 0, 1).step(0.01);
nodeAttributesFolder.add(theme.nodeAttributes, 'draggable');
nodeAttributesFolder.add(theme.nodeAttributes, 'detectable');
const outerStroke = nodeAttributesFolder.addFolder('Outer Stroke');
outerStroke.add(theme.nodeAttributes!.outerStroke, 'width', 0, 50).step(0.1);
outerStroke.addColor(theme.nodeAttributes!.outerStroke, 'color');
outerStroke.add(theme.nodeAttributes!.outerStroke, 'scalingMethod', [
  'fixed',
  'scaled'
]);
outerStroke
  .add(theme.nodeAttributes!.outerStroke, 'minVisibleSize', 0, 100)
  .step(1);

const innerStroke = nodeAttributesFolder.addFolder('Inner Stroke');
innerStroke.add(theme.nodeAttributes!.innerStroke, 'width', 0, 50).step(0.1);
innerStroke.addColor(theme.nodeAttributes!.innerStroke, 'color');
innerStroke.add(theme.nodeAttributes!.innerStroke, 'scalingMethod', [
  'fixed',
  'scaled'
]);
innerStroke
  .add(theme.nodeAttributes!.innerStroke, 'minVisibleSize', 0, 100)
  .step(1);

const halo = nodeAttributesFolder.addFolder('Halo');
halo.add(theme.nodeAttributes!.halo, 'width', 0, 50).step(0.1);
halo.addColor(theme.nodeAttributes!.halo, 'color');
halo.add(theme.nodeAttributes!.halo, 'scalingMethod', ['fixed', 'scaled']);
halo.add(theme.nodeAttributes!.halo, 'strokeColor');
halo.add(theme.nodeAttributes!.halo, 'strokeWidth', 0, 50).step(0.1);
halo.add(theme.nodeAttributes!.halo, 'hideNonAdjacentEdges');
halo.add(theme.nodeAttributes!.halo, 'size', 0, 50).step(0.1);

const badges = nodeAttributesFolder.addFolder('Badges').close();

const icon = nodeAttributesFolder.addFolder('Icon');
icon.add(theme.nodeAttributes!.icon, 'scale', 0, 1).step(0.01);

icon.addColor(theme.nodeAttributes!.icon, 'color');
icon.add(theme.nodeAttributes!.icon, 'minVisibleSize', 0, 100).step(1);
icon.add(theme.nodeAttributes!.icon, 'content', ICONS);

const image = nodeAttributesFolder.addFolder('Image');
image.add(theme.nodeAttributes!.image, 'url', ['flags/nz.svg', 'none']);
image.add(theme.nodeAttributes!.image, 'scale', 0, 10).step(0.1);

createBadgeControls(theme, badges);

const outline = nodeAttributesFolder.addFolder('Outline');
outline.add(theme.nodeAttributes!.outline, 'enabled');
outline.addColor(theme.nodeAttributes!.outline, 'color');
outline.add(theme.nodeAttributes!.outline, 'minVisibleSize', 0, 100).step(1);

// ===== Edge attributes
const edgeAttributesFolder = gui.addFolder('Edge Attributes');
edgeAttributesFolder.addColor(theme.edgeAttributes, 'color');
edgeAttributesFolder.add(theme.edgeAttributes, 'width', 1, 50).step(1);
edgeAttributesFolder.add(theme.edgeAttributes, 'opacity', 0, 1).step(0.01);
edgeAttributesFolder.add(theme.edgeAttributes, 'detectable');

const haloEdgeAttributes = edgeAttributesFolder.addFolder('Halo');
haloEdgeAttributes.add(theme.edgeAttributes!.halo, 'width', 0, 50).step(0.1);
haloEdgeAttributes.addColor(theme.edgeAttributes!.halo, 'color');
haloEdgeAttributes.add(theme.edgeAttributes!.halo, 'scalingMethod', [
  'fixed',
  'scaled'
]);

const outlineEdgeAttributes = edgeAttributesFolder.addFolder('Outline');
outlineEdgeAttributes.add(theme.edgeAttributes!.outline, 'enabled');
outlineEdgeAttributes.addColor(theme.edgeAttributes!.outline, 'color');
outlineEdgeAttributes
  .add(theme.edgeAttributes!.outline, 'minVisibleSize', 0, 100)
  .step(1);

gui.onChange(() => {
  ogma.styles.setTheme(cleanup(theme));
});

const actions = {
  export: async () => {
    const dataStr =
      'data:text/json;charset=utf-8,' +
      encodeURIComponent(JSON.stringify(theme));
    const downloadLink = document.createElement('a');
    downloadLink.setAttribute('href', dataStr);
    downloadLink.setAttribute(
      'download',
      `styles ${new Date().toLocaleString()}.json`.replace(/\s/g, '_')
    );
    downloadLink.style.display = 'none';
    document.body.appendChild(downloadLink);
    downloadLink.click();
    setTimeout(() => {
      document.body.removeChild(downloadLink);
    }, 500);
  },
  import: (evt: Event) => {
    const fileInput = evt.target as HTMLInputElement;
    const file = fileInput.files?.[0];
    if (!file) return;
    const reader = new FileReader();
    reader.onload = e => {
      try {
        const importedTheme = JSON.parse(e.target!.result as string);
        ogma.styles.setTheme(cleanup(importedTheme));
      } catch (err) {
        console.error('Cannot import the file. Reason:', err);
      }
    };
    reader.readAsText(file);
    fileInput.value = ''; // Reset the input so we can load the same file twice
  }
};
gui.add(actions, 'export').name('Export styles');
gui
  .add(actions, 'import')
  .name('Import styles')
  .onChange(() => {
    const fileInput = document.getElementById('file-input') as HTMLInputElement;
    fileInput.click();
  });

function cleanup(inputTheme: ThemeWithSwitches): Theme {
  const theme = JSON.parse(JSON.stringify(inputTheme)) as ThemeWithSwitches;
  // Remove badges that are not enabled
  if (theme.nodeAttributes.badges) {
    Object.entries(theme.nodeAttributes.badges).forEach(([key, badge]) => {
      if (badge.enabled === false) {
        badge.scale = 0;
        badge.stroke!.width = 0;
      } else {
        delete badge.enabled; // Remove the enabled property
      }
    });
  }
  (theme.nodeAttributes.icon! as Icon).content = getIconCode(
    ((theme.nodeAttributes.icon! as Icon).content as string) || ICONS[0]
  );
  if ((theme.nodeAttributes.image as NodeImage).url === 'none')
    (theme.nodeAttributes.image as NodeImage).url =
      'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVR42mP8//8/AwAI/wH+9Q4AAAAASUVORK5CYII=';

  return theme as Theme;
}

function createBadgeControls(theme: ThemeWithSwitches, root: GUI.Folder): void {
  ['topLeft', 'topRight', 'bottomLeft', 'bottomRight'].forEach(position => {
    const badge = theme.nodeAttributes?.badges?.[
      position as keyof NodeAttributes['badges']
    ]! as BadgeWithSwitch;
    const folder = root.addFolder(position);
    folder.add(badge, 'enabled').name('Enabled');
    folder.add(badge?.text, 'content').name('text.content');
    folder.addColor(badge?.text, 'color').name('text.color');
    folder.add(badge?.text, 'scale', 0, 1).step(0.01).name('text.scale');
    folder.add(badge, 'scale', 0, 1).step(0.01);
    folder.addColor(badge, 'color').name('color');
    folder.add(badge, 'scale', 0, 1).step(0.01);
    folder.add(badge, 'minVisibleSize', 0, 100).step(1);
    folder.add(badge.stroke, 'width', 0, 100).step(1).name('stroke.width');
    folder.addColor(badge.stroke, 'color').name('stroke.color');
    folder
      .add(badge.stroke, 'scalingMethod', ['scaled', 'fixed'])
      .name('stroke.scalingMethod');
    folder.close();
  });
}

// helper routine to get the icon HEX code
function getIconCode(icon: string) {
  placeholder.className = `icon-${icon}`;
  const content = getComputedStyle(placeholder, ':before').content;
  return content[1];
}
html
<!doctype html>
<html>
  <head>
    <meta charset="utf-8" />
    <link
      href="https://cdn.jsdelivr.net/npm/lucide-static@0.483.0/font/lucide.css"
      rel="stylesheet"
    />
    <link type="text/css" rel="stylesheet" href="styles.css" />
  </head>

  <body>
    <div id="graph-container"></div>
    <span id="icon-placeholder"></span>
    <script src="index.ts"></script>
  </body>
</html>
css
html {
  margin: 0;
  overflow: hidden;
}

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

#gui-container {
  overflow: auto;
  width: 400px;
  height: 100vh;
  position: absolute;
  top: 0px;
  right: 0;
}

.menu {
  border: 1px solid #ddd;
  width: 80%;
  font-size: 14px;
}
ts
import { ThemeWithSwitches } from './types';

const fontName = 'lucide';

export const ICONS = [
  'shield',
  'chevron-down',
  'folder',
  'target',
  'bug',
  'martini',
  'star',
  'glasses',
  'cloud',
  'arrow-left',
  'user',
  'message-square',
  'play',
  'hammer'
];

export const theme: ThemeWithSwitches = {
  nodeAttributes: {
    color: '#145DA0',
    radius: 5,
    opacity: 1,
    draggable: true,
    detectable: true,
    outerStroke: {
      width: 1,
      color: '#2E8BC0',
      scalingMethod: 'fixed',
      minVisibleSize: 1
    },
    innerStroke: {
      width: 1,
      color: '#ffffff',
      scalingMethod: 'fixed',
      minVisibleSize: 1
    },
    halo: {
      width: 20,
      color: '#dedede',
      scalingMethod: 'fixed',
      strokeColor: '#bbb',
      strokeWidth: 1,
      hideNonAdjacentEdges: true,
      size: 5
    },
    badges: {
      topLeft: {
        enabled: true,
        text: {
          content: 'Tl',
          color: '#ffffff',
          scale: 0.6
        },
        stroke: {
          width: 0.3,
          color: '#ffffff',
          scalingMethod: 'scaled'
        },
        scale: 0.5,
        color: '#145DA0',
        minVisibleSize: 5
      },
      topRight: {
        enabled: false,
        text: {
          content: 'Tr',
          color: '#ffffff',
          scale: 0.6
        },
        stroke: {
          width: 0.3,
          color: '#ffffff',
          scalingMethod: 'scaled'
        },
        scale: 0,
        color: '#145DA0',
        minVisibleSize: 5
      },
      bottomLeft: {
        enabled: false,
        text: {
          content: 'Bl',
          color: '#ffffff',
          scale: 0.6
        },
        stroke: {
          width: 0.3,
          color: '#ffffff',
          scalingMethod: 'scaled'
        },
        scale: 0,
        color: '#145DA0',
        minVisibleSize: 5
      },
      bottomRight: {
        enabled: false,
        text: {
          content: 'Br',
          color: '#ffffff',
          scale: 0.6
        },
        stroke: {
          width: 0.3,
          color: '#ffffff',
          scalingMethod: 'scaled'
        },
        scale: 0.5,
        color: '#DA5DA0',
        minVisibleSize: 5
      }
    },
    image: {
      url: 'flags/nz.svg',
      scale: 2.5
    },
    icon: {
      scale: 0.35,
      color: '#ffffff',
      minVisibleSize: 5,
      font: fontName,
      content: ICONS[0]
    },
    outline: {
      enabled: false,
      color: '#ffffff',
      minVisibleSize: 1
    },
    shape: 'circle'
  },
  edgeAttributes: {
    color: '#145DA0',
    width: 1,
    opacity: 1,
    detectable: true,
    halo: {
      width: 0,
      color: '#dedede',
      scalingMethod: 'fixed'
    },
    outline: {
      enabled: false,
      color: '#ffffff',
      minVisibleSize: 1
    }
  },
  hoveredEdgeAttributes: { color: '#2E8BC0', width: 2, opacity: 1 },
  hoveredNodeAttributes: { color: '#2E8BC0', radius: 6, opacity: 1 },
  selectedEdgeAttributes: { color: '#0C2D48', width: 3, opacity: 1 },
  selectedNodeAttributes: { color: '#0C2D48', radius: 6, opacity: 1 }
};

export const SHAPES = [
  'none',
  'circle',
  'square',
  'cross',
  'diamond',
  'star',
  'equilateral',
  'triangle',
  'triangleDown',
  'triangleRight',
  'triangleLeft',
  'hexagon'
];
ts
import { Badge, NodeAttributes, Theme } from '@linkurious/ogma';

export interface BadgeWithSwitch extends Badge {
  enabled?: boolean;
}

export interface ThemeWithSwitches extends Theme {
  nodeAttributes: NodeAttributes & {
    badges?: {
      bottomLeft?: BadgeWithSwitch;
      bottomRight?: BadgeWithSwitch;
      topRight?: BadgeWithSwitch;
      topLeft?: BadgeWithSwitch;
    };
  };
}