0.3.0 • Published 4 years ago

tiffon-plexus v0.3.0

Weekly downloads
-
License
Apache-2.0
Repository
-
Last release
4 years ago

plexus

A React component for directed graphs.

NPM tiffon-plexus Apache 2.0 License

About

We needed to render directed graphs in Jaeger UI for the trace comparison feature and the experimental Trace Graph view. So, we surveyed the options available for generating directed graphs, in JavaScript. Our considerations included:

  • The more readable the graphs, the better
  • Try not to step outside of React
  • Complex layouts within nodes should be supported
  • We should not need to specify the width and height of nodes (but, resizing nodes doesn't need to be supported)
  • We want lots of options for styling and adding interactivity

We found the landscape to be very impressive, but none of the existing options seemed to fit our key takeaways:

  • The venerable GraphViz does a fantastic job with layouts, i.e. positioning nodes and routing edges
  • Regarding complex layouts within nodes, using HTML is second nature while things get complicated fast with SVG or canvas and GraphViz is not sufficiently expressive for our needs
  • React is great for things like defining and managing interactivity and creating and styling complex layouts, so let's leverage it as much as we can

The approach we've taken is pretty much a main-line of the points above:

  • Use GraphViz to determine node positions and route edges
  • Use React for everything else

To break this down a bit further:

  • GraphViz
    • Generally the dot layout engine in GraphViz is all we need for generating graph layouts
    • Sometimes it makes sense to use dot to position nodes, only, and use neato to route edges
  • React
    • Use HTML to render content within nodes
    • Use SVG to render edges
    • Use either HTML or SVG to render supplemental layers of graph elements
    • Use standard React patterns for adding interactivity

The excellent viz.js is used, in a WebWorker, to generate GraphViz as plain-text output which is then parsed and provided to a React component which does the rendering.

Note: The viz.js repository on GitHub is archived.However, it still works great. And, likely could be even better if we upgrade to the last published version (#339).

Install

# Yarn
yarn add @jaegertracing/plexus

# NPM
npm install --save @jaegertracing/plexus

Quick start

Import

import * as React from 'react';

import { LayoutManager } from 'plexus';
// TODO(joe): Update import after killing `DirectedGraph`
import Digraph from 'plexus/Digraph';

Data

For each node in the graph you need to define an object with a key field that uniquely identifies the node. We'll call this a vertex object. The vertices can have additional fields; below we've added name fields to our vertices.

const vertices = [
  { key: 'web', name: 'web-app : login' },
  { key: 'users', name: 'user-store : get-user' },
  { key: 'cache', name: 'cache : get' },
  { key: 'db', name: 'db : get-user' },
  { key: 'auth', name: 'auth : login' },
];

For each edge in the graph, you need to define an object with to and from fields, the value of which map to the key fields on the vertices. This defines the which vertex is the head and which is the tail. Edges objects can have additional fields.

// Edges must refer to the `key` field of vertices.
const edges = [
  { from: 'web', to: 'users' },
  { from: 'web', to: 'auth' },
  { from: 'users', to: 'cache' },
  { from: 'users', to: 'db' },
];

LayoutManager

The LayoutManager generates the layout for the graph, i.e. it determines the node positions and the paths of the edges. Options can be passed that will affect the layout. See LayoutManager options for details.

const lm = new LayoutManager({ useDotEdges: true, rankdir: 'TB', ranksep: 1.1 });

Digraph

The bulk of the public API is the Digraph component.

Below, we use the Digraph component to create a graph from the vertices and edges we defined, above. We set some styles, pass in the LayoutManager and configure the layers of the graph.

const simpleGraph = (
  <Digraph
    edges={edges}
    vertices={vertices}
    setOnGraph={{
      style: {
        fontFamily: 'sans-serif',
        height: '100%',
        position: 'fixed',
        width: '100%',
      },
    }}
    layoutManager={lm}
    measurableNodesKey="nodes"
    layers={[
      {
        key: 'edges',
        edges: true,
        layerType: 'svg',
        defs: [{ localId: 'edge-arrow' }],
        markerEndId: 'edge-arrow',
      },
      {
        key: 'nodes',
        layerType: 'html',
        measurable: true,
        renderNode: (vertex: TVertex) => vertex.name,
        setOnNode: { style: { padding: '1rem', whiteSpace: 'nowrap', background: '#e8e8e8' } },
      },
    ]}
  />
);

render(simpleGraph, document.querySelector('#root'));

Result

Combined, the above code snippets render the following graph:

alt text

Concepts

The elements of a plexus graph are defined as a series of layers. These establish how the nodes and edges will be rendered. A layer can generate either HTML elements or SVG elements and can render either nodes or edges. Each type of element, i.e. nodes or edges, can be represented by more than one layer.

plexus uses GraphViz (via the viz.js package) to generate the layout for graphs. This layout information is then combined with the edge and vertex data and passed to the layers for rendering.

The life cycle of a graph in plexus is as following:

  1. Render the nodes (but do not show them) with an initial position of 0, 0
  2. Measure the nodes so their size can be accounted for
  3. Generate the layout of the graph
  4. Render the layers of elements

In step 1, we render nodes but they are not shown to the user. This set of nodes is used to determine the width and height of each node so their sizes can be accounted for by GraphViz. This layer is unique in that it must be able to render nodes without knowing anything about the layout of the graph. We can say this layer of nodes is measurable, and every plexus graph requires one measurable node layer.

Steps 2 and 3 happen behind the scenes and we can basically ignore them. (The layout does have some configuration options that are covered, below.)

Step 4 is where we bring the graph to life.

Edge and vertex data

Within plexus, each vertex must be uniquely identified by a key field of type either string or number. This must be unique within the vertices of a given graph. Edges refer to these keys.

type TVertexKey = string | number;

// TODO(jeo): change the default type in types/index.tsx
type TVertex<T = Record<string, unknown>> = T & {
  key: TVertexKey;
};

type TEdge<T = Record<string, unknown>> = T & {
  from: TVertexKey;
  to: TVertexKey;
};

The data underlying edges and vertices are arrays of these types:

// type is TVertex<{ name: string }>
const vertices = [
  { key: 'web', name: 'web-app : login' },
  { key: 'users', name: 'user-store : get-user' },
  // etc...
];

// type is simply TEdge<{}>
const edges = [
  { from: 'web', to: 'users' },
  { from: 'web', to: 'auth' },
  // etc...
];

Layers

There are three types of layers:

  • Edges
  • Nodes
  • Measurable nodes

Layers are configured through plain JavaScript objects and the layers prop on the Digraph component.

A layer can generate either HTML elements or SVG elements but not a combination of the two.

Note: We didn't see a practical reason to support edge layers that generate HTML. So, only SVG is supported for edge layers, at this time.

Layers have a containing element to group the elements they render. For HTML layers this is a div; for SVG layers this is a g.

The ordering of layers in the document matches the order in which they're defined in the layers prop on the Digraph.

Measurable nodes

As noted in the description of the lifecycle, every plexus graph must contain one measurable nodes layer. This layer is rendered before the layout is generated so the size of the nodes can be accounted for in the layout.

This layer can be either HTML or SVG, and the value of the layer's key must also be set to the measurableNodesKey prop on the Digraph component.

By default, the size of a node is based on the dimensions of the wrapper for the node after it's been rendered to the document: a div for HTML nodes and a g for SVG nodes. This default behavior can be overridden via the measureNode field on the layer configuration object.

Layers group

Layers can be grouped by their type: HTML or SVG. This is mainly only relevant if zoom is enabled or if you want to set props on a common container element for several layers. If zoom is enabled, the zoom transform will be applied to the common container of the layers instead of to each individual layer.

Note: Nesting groups is not supported.

setOn* props factories

plexus provides hooks to define or generate props for the elements in the graph and their containers. For instance, the setOnGraph prop of the Digraph component allows props to be defined or generated for the root <div> element of the graph.

Generally, the value of these can be either an object of props to set on the target element, a function which will generate either null or an object of props to set on the target, or an array of either of these.

const graphClassName = { className: 'LeGraphOlogy' };
// ignoring the parameters that are passed to the factory function, for now...
const generatePaddingStyle = () => {
  style: {
    padding: `${(Math.random() * 10).toFixed()}px`;
  }
};

// All three of these are valid:

// Set only the CSS class
const ok = (
  <Digraph
    edges={edges}
    vertices={vertices}
    setOnGraph={graphClassName}
    // etc...
  />
);

// Set only the random padding
const alsoOk = (
  <Digraph
    edges={edges}
    vertices={vertices}
    setOnGraph={generatePaddingStyle}
    // etc...
  />
);

// Set both the CSS class and the random padding
const allOfTheAbove = (
  <Digraph
    edges={edges}
    vertices={vertices}
    setOnGraph={[graphClassName, generatePaddingStyle]}
    // etc...
  />
);

API

Input

TVertexKey

The type for the key field on vertices.

type TVertexKey = string;

TVertex

The type for the data underlying vertices.

type TVertex<T = {}> = T & {
  key: TVertexKey;
};

TEdge

The data that underlies edges in a graph.

type TEdge<T = {}> = T & {
  from: TVertexKey;
  to: TVertexKey;
  isBidirectional?: boolean;
};

Data augmented with layout information

TLayoutVertex

The underlying vertex data with layout information for a given vertex.

type TLayoutVertex<T = {}> = {
  vertex: TVertex<T>;
  height: number;
  left: number;
  top: number;
  width: number;
};

TLayoutEdge

The combination of the underlying edge data (for a single edge) and the path information for a given edge.

type TLayoutEdge<T = {}> = {
  edge: TEdge<T>;
  pathPoints: [number, number][];
};

TLayoutGraph

Indicates the size and scale of the full graph after it's been laid out.

type TLayoutGraph = {
  height: number;
  scale: number;
  width: number;
};

Externally exposed graph state

Various aspects of the state of the plexus graph are made available to props factories and render functions.

TRendererUtils

These utils are made available to the props factories (setOn...) and the render fields of the layer configuration objects.

The specific object which serves as the TRendererUtils that is made available does not change; referential equality is maintained throughout the life of a plexus graph. Therefore, these utils don't trigger updates to components (such as when the zoom transform changes).

FieldType and description
getLocalId(name: string) => string
Takes in a string and prefixes it to scope the string to the current plexus graph. This effectively allows for IDs that are unique within the document given the name parameter is unique within a graph. 
getZoomTransform() => ZoomTransform
Returns the current D3 zoom transform. See https://github.com/d3/d3-zoom#zoom-transforms for details.Note: A reference to this function can be used to access the current zoom, at any time. For instance, if we have a node that shows a normal scale view of itself on hover, this function can be used to restrict the hover effect to only happen when the graph is actually at a reduced scale. 

TExposedGraphState

This type gives access to the graph's current state, such as the current phase or the layout vertices. This is available to the container-level prop factories and the render field for TDefEntry elements of SVG layer configurations.

FieldType and description
edgesTEdge[]
The user provided edge data underlying the edges in the graph. 
layoutEdgesTLayoutEdge[] \| null
The edge data and the layout data. This is null if the layout is not yet generated. 
layoutGraphTLayoutGraph \| null
The dimensions of the graph. null if the layout is not yet generated. 
layoutPhaseELayoutPhase
The current phase of the graph. 
layoutVerticesTLayoutVertex[] \| null
The vertex data and the layout data. This is null if the layout is not yet generated. 
renderUtilsTRendererUtils
Utils for converting an ID local to the graph to globally unique in the document and fetching the current zoom transform. 
verticesTVertex[]
The user provided vertex data underlying the nodes in the graph. 
zoomTransformZoomTransform
The current zoom transform on the graph. 

ELayoutPhase is an enum of the phases of the graph layout process.

enum ELayoutPhase {
  NoData = 'NoData',
  CalcSizes = 'CalcSizes',
  CalcPositions = 'CalcPositions',
  CalcEdges = 'CalcEdges',
  Done = 'Done',
}

TContainerPropsSetter

The type of the props factories for containers that allows props to be defined or generated.

This type is either a static collection of props to set on the container, a factory function to generate props (or null), or an array of either of these values.

type TContainerPropsSetter =
  | Record<string, unknown>
  | ((input: TExposedGraphState) => Record<string, unknown> | null)
  | (Record<string, unknown> | ((input: TExposedGraphState) => Record<string, unknown> | null))[];

LayoutManager options

The LayoutManager supports the following configuration options:

NameType and description
totalMemorynumber
This affects the total memeory available for the GraphViz Emscripten module instance. It's useful if you're hitting memory allocation errors. See totalMemory reference. The value should be a power of two. 
useDotEdgesboolean = false
When true the dot edges are used; i.e. generating neato edge paths is skipped. 
splinesstring = "true"
GraphViz splines graph attribute. 
sepnumber = 0.5
GraphViz sep graph attribute, which defines the space margin around nodes. 
rankdir'TB' \| 'LR' \| 'BT' \| 'RL' = 'LR'
GraphViz rankdir graph attribute, which defines the orientation of the layout. 
ranksepnumber = 5
GraphViz ranksep graph attribute, which defines the minimum distance between levels of nodes. 
nodesepnumber = 1.5
GraphViz nodesep graph attribute, which establishes the minimum distance between two adjacent nodes in the same level. 

Digraph props

NameType and description
classNamestring
Added to the root-most div for the graph 
classNamePrefixstring = "plexus"
Applied as a CSS class and a prefix to element specific CSS classes for all elements within the graph. 
edgesTEdge[]
RequiredThe data underlying the edges in the graph. 
layersTLayer[]
RequiredThe layers configuration. See below for details. 
layoutManagerLayoutManager
RequiredThe LayoutManager for this graph. Each graph should have it's own instance. 
measurableNodesKeystring
RequiredThis should be the key of the measurable nodes layer. It is required and will throw a runtime error if the key from the measurable nodes layer does not match this prop. 
minimapboolean
Boolean flag to enable the minimap. If enabled, the minimapClassName should be set to something that will style it. **The minimap has no builtin styling. 
minimapClassNamestring
Added to the root-most container on the minimap. 
setOnGraphTContainerPropsSetter
An optional prop that allows props to be defined for the root-most div element of the graph. 
styleReact.CSSProperties
Set on the root-most container div. 
verticesTVertex[]
RequiredThe data underlying the vertices in the graph. 
zoomboolean
Boolean flag to enable zoom and pan. 

Layer configuration objects

Common to all layers

Configuration fields common to all layers.

NameType and description
layerType"html" \| "svg"
RequiredIndicates the type of elements the layer will render. This determines the type of the container the elements are grouped into.Note: This field is not required (or even allowed) on layers that are within HTML or SVG layers group. The layer inherits the value from the group. 
keystring
RequiredThis is used as the key prop on the resultant JSX and is required on all layers or layer groups. 
setOnContainerTContainerPropsSetter
An optional field that allows props to be defined or generated for the container element of the layer. 

Common to SVG layers, only

This configuration field is available only on SVG layers.

NameType and description
defsTDefEntry[] See below for details on the TDefEntry type.
defs allows you to add elements to a <defs> within an SVG layer or group of SVG layers. The main use of defs is to define markers for the edges. See TDefEntry, below, for details on configuring defs. 

The TDefEntry type is defined as follows:

FieldType and description
localIdstring
RequiredThe ID part that must be unique within a graph. localId be unique within a Digraph instance. localId will then be prefixed with an ID that is unique to the instance Digraph, resulting in the final ID which is unique within the document. This final ID is then passed to renderEntry as the third argument. 
renderEntryTRenderDefEntryFn See below for details on the function signature.
Provide a render function for the element that will be added to the <defs>.Note: The fallback renderEntry function (i.e. the default value for this field) will return a <marker> suitable to be the marker-end reference on an edge's <path>. This <marker> will result in an arrow head. 
setOnEntryTContainerPropsSetter
Specify props to be passed as the second argument to the renderEntry function. See TContainerPropsSetter for details on this field's type. 

And, the signature for the renderEntry function is:

type TRenderDefEntryFn = (
  graphState: TExposedGraphState,
  entryProps: Record<string, unknown> | null,
  id: string
) => React.ReactElement;
ArgumentType and description
0graphStateTExposedGraphState
The current state of the graph. See TExposedGraphState for details. 
1entryPropsRecord<string, unknown> | null
The the result of evaluating setOnEntry. 
2idstring
An ID, unique within the document, to be applied to the root-most element being returned from renderEntry. 

Measurable nodes layer

Digraph requires one measurable nodes layer.

In addition to the common layer configuration fields, the following fields are also available:

NameType and description
measurabletrue
RequiredIndicates the layer of nodes should be used for determining the size of the nodes. 
setOnNodeTMeasurableNodePropsSetter See below for details on this type.
Allows props to be defined or generated for the container of the node. This is a <div> for HTML layers and a <g> for SVG layers. Note: The resultant props are applied to the container element; they are not passed on to the renderNode factory. 
renderNodeTRenderMeasurableNodeFn See below for details on this type.
RequiredA factory function that is used to generate nodes from the vertices prop on the Digraph component. The generated node will be used to determine the size of the nodes, which is taken into account when laying out the graph. renderNode is invoked for each TVertex. The TLayoutVertex will be null until the graph layout is available. This function will have access to the TRenderUtils, which means it can access the current zoom transform, but it is not redrawn when the zoom transform changes. 
measureNodeTMeasureNodeFn See below for details on this type.
Overrides the default measuring of nodes. 

The types for setOnNode and renderNode are distinct from the corresponding fields on a non-measurable nodes layer in that the first argument is the TVertex, and the TLayoutVertex argument is only available after initial render.

The type for setOnNode is similar to that of setOnContainer in that the value can also be either an object of props, a factory function, or an array of either.

type TMeasurableNodePropsSetter =
  | Record<string, unknown>
  | TMeasurableNodePropsFn
  | (TMeasurableNodePropsFn | Record<string, unknown>)[];

type TMeasurableNodePropsFn = (
  vertex: TVertex,
  utils: TRendererUtils,
  layoutVertex: TLayoutVertex | null
) => Record<string, unknown> | null;

The type for the renderNode field.

type TRenderMeasurableNodeFn = (
  vertex: TVertex,
  utils: TRendererUtils,
  layoutVertex: TLayoutVertex | null
) => React.ReactNode;

The type for measureNode.

type TMeasureNodeFn = (vertex: TVertex, utils: TMeasureNodeUtils) => { height: number; width: number };

type TMeasureNodeUtils = {
  layerType: 'html' | 'svg';
  getWrapperSize: () => { height: number; width: number };
  getWrapper: () => TOneOfTwo<{ htmlWrapper: HTMLDivElement | null }, { svgWrapper: SVGGElement | null }>;
};

Nodes layer

Any number of nodes layers can be configured for a Digraph.

In addition to the common layer configuration fields, the following fields are also available:

NameType and description
setOnNodeTNodePropsSetter
Allows props to be defined or generated for the container of the node. This is a <div> for HTML layers and a <g> for SVG layers. Note: The resultant props are applied to the container element; they are not passed on to the renderNode factory. 
renderNodeTRenderNodeFn
RequiredA factory function that is used to generate nodes from the TLayoutVertex data. renderNode is invoked for each TLayoutVertex. Unlike measurable nodes layers, the TLayoutVertex will always be available. This function will have access to the TRenderUtils, which means it can access the current zoom transform, but it is not redrawn when the zoom transform changes. 

The types for setOnNode and renderNode are distinct from the corresponding fields on a measurable nodes layer in that the layoutVertex argument is always available.

The type for setOnNode is similar to that of setOnContainer in that the value can also be either an object of props, a factory function, or an array of either.

type TNodePropsSetter = Record<string, unknown> | TNodesPropsFn | (TNodesPropsFn | Record<string, unknown>)[];

type TNodesPropsFn = (
  layoutVertex: TLayoutVertex | null,
  utils: TRendererUtils
) => Record<string, unknown> | null;

The type for the renderNode field.

type TRenderNodeFn = (layoutVertex: TLayoutVertex, utils: TRendererUtils) => React.ReactNode;

Edges layer

Any number of edges layers can be configured for a Digraph. Edges layers are more restrictive (or less mature) than nodes layers, at present:

  • The layerType of edges layers must be "svg"
  • Edges layers do not afford a renderEdge equivalent to the renderNode.

Thus, edges are less configurable than nodes (for now). If you need additional functionality, please file a ticket.

The builtin renderer for edges draws a path based on the pathPoints field of the TLayoutEdge being rendered.

In addition to the common layer configuration fields, the following fields are also available:

NameType and description
edgestrue
RequiredIndicates the layer will render edges. 
layerType"svg"
RequiredEdges must be SVG layers. 
markerEndIdstring
This field should refer to an element to use as the marker-end for the edge <path>. A typical scenario matches the markerEndId to the localId of a TDefEntry. Each of the localId and markerEndId are passed through the getLocalId() util (TODO: reaname the util) to generate an ID unique within the document. 
markerStartIdstring
The marker-start equivalent of the markerEndId field. 
setOnEdgeTEdgePropsSetter See below for details on this type.
Allows props to be defined or generated for the <path>. Unlike nodes, edges are not wrapped in a container. So, the resultant props are applied directly to the <path>. 
defsTDefEntry[]
For edges, this is generally used to define arrows (or other markers) in order to indicate directionality. See TDefEntry for additional details.The default functionality of a TDefEntry is suitable to be the markerEnd of an edge. To define an arrow head marker on an edge layer, simple set the markerEndId of the layer to the localId of the TDefEntry:{ defs: [{ localId: 'arrow' }], markerEndId: 'arrow', ...otherProps } 

The type for setOnEdge is similar to that of setOnContainer in that the value can also be either an object of props, a factory function, or an array of either.

type TEdgePropsSetter = Record<string, unknown> | TEdgesPropsFn | (TEdgesPropsFn | Record<string, unknown>)[];

type TEdgesPropsFn = (edge: TLayoutEdge, utils: TRendererUtils) => Record<string, unknown> | null;

HTML and SVG layers group

An HTML layers group can be used to group multiple HTML layers together. And, the SVG layers group does the same for SVG layers.

Using a group is mainly only going to be useful if zoom is enabled on the Digraph or if you want to set props on a container that is common to the layers within the group.

Regarding zoom, using a group will cause the current zoom transform to be applied once to the entire group instead of individually to each of the layers within the group.

Note: Layers configured within the layers field of a group of layers inherit the layerType from the group. The individual layers should not have a layerType defined.

type THtmlLayersGroup = {
  key: string;
  layerType: 'html';
  setOnContainer?: TSetOnContainer;
  layers: (TMeasurableNodesLayer | TNodesLayer)[];
};

type TSvgLayersGroup = {
  key: string;
  layerType: 'svg';
  setOnContainer?: TSetOnContainer;
  defs?: TDefEntry[];
  layers: (TEdgesLayer | TMeasurableNodesLayer | TNodesLayer)[];
};

Builtin props factories

TODO(joe): remove scaledStrokeWidth since it's no longer necessary

plexus ships with a few functions that are suitable for use with the setOnContainer field.

classNameIsSmall

This utility returns { className: 'is-small' } if the graph is zoom out to a small scale. If added to a setOnContainer field or the setOnGraph prop of the Digraph it will add the CSS class to the container when the graph is zoomed out to a small scale.

This util can be used to hide text when it would be too small to read:

.demo-graph.is-small .demo-node {
  color: transparent;
}
<Digraph
  edges={edges}
  vertices={vertices}
  setOnGraph={[{ className: 'demo-graph' }, classNameIsSmall]}
  layoutManager={lm}
  measurableNodesKey="nodes"
  layers={[
    {
      key: 'nodes',
      layerType: 'html',
      measurable: true,
      // Alternatively, it can be used on the nodes layer
      // setOnContainer: classNameIsSmall,
      renderNode: (vertex: TVertex) => vertex.name,
      setOnNode: { className: 'demo-node' },
    },
    {
      key: 'edges',
      edges: true,
      layerType: 'svg',
      defs: [{ localId: 'arrow' }],
      markerEndId: 'arrow',
    },
  ]}
/>

Alternatively, it could be set on the setOnContainer field of the nodes layer.

scaleProperty.opacity

This utility will generate a style prop with the opacity reduced as the view zooms out.

In the following example, the opacity of the edges will be reduced as the view is zoomed out.

<Digraph
  edges={edges}
  vertices={vertices}
  layoutManager={lm}
  measurableNodesKey="nodes"
  layers={[
    {
      key: 'nodes',
      layerType: 'html',
      measurable: true,
      renderNode: (vertex: TVertex) => vertex.name,
    },
    {
      key: 'edges',
      edges: true,
      layerType: 'svg',
      setOnContainer: scaleProperty.opacity,
      defs: [{ localId: 'arrow' }],
      markerEndId: 'arrow',
    },
  ]}
/>

scaleProperty.strokeOpacity

This is the same as scaleProperty.opacity (above) but it reduces the stroke-opacity when the view is zoomed out.

scaleProperty

scaleProperty is a factory function for creating utilities like scaleProperty.opacity that interpolate the value of a CSS property based on the scale of the graph's zoom transform. For instance, scaleProperty.opacity reduces the opacity as the scale of the graph reduces (i.e. as the user zooms out).

The typedef for the factory is:

function scaleProperty(
    property: keyof React.CSSProperties,
    valueMin: number = 0.3,
    valueMax: number = 1,
    expAdjuster: number = 0.5
) => (graphState: TExposedGraphState) => React.CSSProperties;

With the default values, the property will approach 0.3 as the scale of the zoom transform approaches 0. The expAdjuster is an exponent applied to the linear change. By default, the interpolation is based on the square root of the linear change.

If you need something more expressive, take a look at packages/plexus/src/Digraph/utils.tsx, which scaleProperty wraps.

Recipes

TODO:

  • Recipe for borders that don't diminish when the view is zoomed out
  • Recipe for node outlines, for emphasis, that don't diminish when the view is zoomed out
  • Recipe for coloring edges based on their direction
  • Recipe for animating edges to indicate directionality
  • Recipe for showing a 1x scale view of a node, on hover, only when the graph is less than N scale

Arrow heads

An arrow head, to indicate directionality, can be added to an edges layer by adding a TDefEntry which uses the builtin renderer. The localId of the TDefEntry must match the markerEndId of the edges layer.

const edgesLayer = {
  key: 'edges',
  edges: true,
  layerType: 'svg',
  defs: [{ localId: 'edge-arrow' }],
  markerEndId: 'edge-arrow',
};

UX + edges

Edges can be quite thin, visually, which doesn't really lend itself to adding interactivity. To mitigate this, two layers of edges can be used. The lower layer is the visible layer which has a fairly thin stroke. A second edges layer above that layer an have a larger stroke-width but the stroke color is set to transparent. On this larger stroke we add our interactivity.

const edgeLayersGroup = {
  key: 'edges-layers',
  layerType: 'svg',
  defs: [{ localId: 'arrow-head' }],
  layers: [
    {
      key: 'edges',
      markerEndId: 'arrow-head',
      edges: true,
    },
    {
      key: 'edges-pointer-area',
      edges: true,
      setOnContainer: { style: { opacity: 0, strokeWidth: 4 } },
      setOnEdge: layoutEdge => ({
        onMouseOver: () => console.log('mouse over', layoutEdge),
        onMouseOut: () => console.log('mouse out', layoutEdge),
      }),
    },
  ],
};