Skip to content
On this page

Guide

Motivation

D3 is powerful but requires some effort to get good results. Furthermore, managing a robust lifecycle with extensive customization options is downright difficult. This library aims to make graph building a declarative task and provide an abstraction layer for the complexity of D3.

It does so by using an extensive configuration as the basis for creating graphs. Everything should be configurable in a declarative way that is understandable without insight into the inner workings of D3.

In addition, models of graphs should be type-safe and extensible. This library allows for custom node and link data types that extend the default model with custom properties. These custom properties can then be used anywhere in the configuration.

Lastly, this library is framework-agnostic. A graph's container element can be retrieved by any means, including Vue's refs, React's refs, Angular's ViewChild, or the old and trustworthy document.gelElementById. Just do not forget to integrate the graph in the framework's lifecycle.

Installation

bash
# with yarn
yarn add d3-graph-controller

# or with npm
npm install d3-graph-controller

# or with pnpm
pnpm add d3-graph-controller

Usage

The data model of a graph can be customized to fit any need. The following sections show a model with two node types, primary and secondary, custom node radius and link length as well as dynamic force strength.

Type Tokens

First, define the types of nodes the graph may contain.

ts
export type CustomType = 'primary' | 'secondary'

Node

Then you can enhance the GraphNode interface with custom properties that can be accessed later on.

ts
import { GraphNode } from 'd3-graph-controller'

export interface CustomNode extends GraphNode<CustomType> {
  radius: number
}

Analogous to nodes, GraphLink can be extended. While not shown in the example below, GraphLink can have specific node types for source and target.

ts
import { GraphLink } from 'd3-graph-controller'

export interface CustomLink extends GraphLink<CustomType, CustomNode> {
  length: number
}

Config

The config can then use the custom types.

ts
import { defineGraphConfig } from 'd3-graph-controller'

const config = defineGraphConfig<CustomType, CustomNode, CustomLink>({
  nodeRadius: (node: CustomNode) => node.radius,
  simulation: {
    forces: {
      centering: {
        strength: (node: CustomNode) => (node.type === 'primary' ? 0.5 : 0.1),
      },
      link: {
        length: (link: CustomLink) => link.length,
      },
    },
  },
})

Model

The actual model can be created using the helper methods seen below. They are type safe and support custom properties.

ts
import { defineGraph, defineLink, defineNode } from 'd3-graph-controller'

const a = defineNode<CustomType, CustomNode>({
  id: 'a',
  type: 'primary',
  isFocused: false,
  color: 'green',
  label: {
    color: 'black',
    fontSize: '1rem',
    text: 'A',
  },
  radius: 64,
})

const b = defineNode<CustomType, CustomNode>({
  id: 'b',
  type: 'secondary',
  isFocused: false,
  color: 'blue',
  label: {
    color: 'black',
    fontSize: '1rem',
    text: 'B',
  },
  radius: 32,
})

const aToB = defineLink<CustomType, CustomNode, CustomNode, CustomLink>({
  source: a,
  target: b,
  color: 'red',
  label: {
    color: 'black',
    fontSize: '1rem',
    text: '128',
  },
  length: 128,
})

const graph = defineGraph<CustomType, CustomNode, CustomLink>({
  nodes: [a, b],
  links: [aToB],
})

Controller

The last step is putting it all together and creating the controller.

ts
import { GraphController } from 'd3-graph-controller'

// Any HTMLDivElement can be used as the container
const container = document.getElementById('graph') as HTMLDivElement

const controller = new GraphController(container, graph, config)

TIP

Do not forget to call controller.shutdown() when the graph is no longer required or your component will be destroyed.

Styling

The library provides default styles, which need to be imported manually.

ts
import 'd3-graph-controller/default.css'

In addition, the properties color and fontSize of nodes and links accept any valid CSS value. This allows you to use dynamic colors with CSS variables.

css
:root {
  --color-primary: 'red';
}
ts
import { defineNodeWithDefaults } from 'd3-graph-controller'
import 'd3-graph-controller/default.css'

const a = defineNodeWithDefaults({
  type: 'node',
  id: 'a',
  label: {
    color: 'black',
    fontSize: '2rem',
    text: 'A',
  },
  color: 'var(--color-primary)',
})

For customization of the default theme, the custom CSS properties --color-stroke and --color-node-stroke can be used.

Classes

Graphs can also be styled using CSS. For this purpose, various classes are defined. Reference the table below for a description of all available classes.

ClassElementDescription
graphContainer of the graphAdded to the graph's container on initialization.
linkPath of a link
link__labelLabel of a link
nodeCircle of a node
node__labelLabel of a node
focusedFocused nodeApplied to a focused node. Recommended usage is .node.focused.
draggedDragged nodes or canvasAdded to a node or the canvas while it is being dragged. Sets the cursor to grabbing.

Default Stylesheet

Usually, importing the default stylesheet and configuring variables should be enough to fit all needs. If a full custom styling is required, the default stylesheet as seen below might act as a template.

css
.graph,
.graph > svg {
  display: block;
}

.graph {
  height: 100%;
  touch-action: none;
  width: 100%;
}

.graph * {
  -webkit-touch-callout: none !important;
  -webkit-user-select: none !important;
  -moz-user-select: none !important;
  -ms-user-select: none !important;
  user-select: none !important;
}

.link {
  fill: none;
  stroke-width: 4px;
}

.node {
  --color-stroke: var(--color-node-stroke, rgba(0, 0, 0, 0.5));

  cursor: pointer;
  stroke: none;
  stroke-width: 2px;
  transition: filter 0.25s ease, stroke 0.25s ease, stroke-dasharray 0.25s ease;
}

.node:hover:not(.focused) {
   filter: brightness(80%);
   stroke: var(--color-stroke);
   stroke-dasharray: 4px;
}

.node.focused {
   stroke: var(--color-stroke);
}

.link__label,
.node__label {
  pointer-events: none;
  text-anchor: middle;
}

.grabbed {
  cursor: grabbing !important;
}

Released under the MIT License.