Skip to content

Commit

Permalink
feat(StateVisualizer): Support customization of state node labels
Browse files Browse the repository at this point in the history
Adds `options.stateVisualizer.node.label` customization api to options object
  • Loading branch information
christopherthielen committed Sep 16, 2020
1 parent f4a53a3 commit d3cfd6e
Show file tree
Hide file tree
Showing 5 changed files with 129 additions and 89 deletions.
120 changes: 77 additions & 43 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@ Visualizes the state tree and transitions in UI-Router 1.0+.

This script augments your app with two components:

1) State Visualizer: Your UI-Router state tree, showing the active state and its active ancestors (green nodes)
1. State Visualizer: Your UI-Router state tree, showing the active state and its active ancestors (green nodes)

- Clicking a state will transition to that state.
- If your app is large, state trees can be collapsed by double-clicking a state.
- Supports different layouts and zoom.
- Supports different layouts and zoom.

2) Transition Visualizer: A list of each transition (from one state to another)
2. Transition Visualizer: A list of each transition (from one state to another)

- Color coded Transition status (success/error/ignored/redirected)
- Hover over a Transition to show which states were entered/exited, or retained during the transition.
Expand All @@ -28,37 +29,37 @@ Register the plugin with the `UIRouter` object.

### Locate the Plugin

- Using a `<script>` tag

Add the script as a tag in your HTML.

```html
<script src="//unpkg.com/@uirouter/visualizer@4"></script>
```
The visualizer Plugin can be found (as a global variable) on the window object.
```js
var Visualizer = window['@uirouter/visualizer'].Visualizer;
```
- Using `require` or `import` (SystemJS, Webpack, etc)

Add the npm package to your project
```
npm install --save @uirouter/visualizer
```
- Use `require` or ES6 `import`:
```js
var Visualizer = require('@uirouter/visualizer').Visualizer;
```
```js
import { Visualizer } from '@uirouter/visualizer';
```
- Using a `<script>` tag

Add the script as a tag in your HTML.

```html
<script src="//unpkg.com/@uirouter/visualizer@4"></script>
```

The visualizer Plugin can be found (as a global variable) on the window object.

```js
var Visualizer = window['@uirouter/visualizer'].Visualizer;
```

- Using `require` or `import` (SystemJS, Webpack, etc)

Add the npm package to your project

```
npm install --save @uirouter/visualizer
```

- Use `require` or ES6 `import`:

```js
var Visualizer = require('@uirouter/visualizer').Visualizer;
```

```js
import { Visualizer } from '@uirouter/visualizer';
```

### Register the plugin

Expand All @@ -79,18 +80,52 @@ var pluginInstance = uiRouterInstance.plugin(Visualizer);

### Configuring the plugin

Optionally you can pass configuration to how the visualizer displays the state tree and the transitions.
You can pass a configuration object when registering the plugin.
The configuration object may have the following fields:

- `state`: (boolean) State Visualizer is not rendered when this is `false`
- `transition`: (boolean) Transition Visualizer is not rendered when this is `false`
- `stateVisualizer.node.label`: (function) A function that returns the label for a node
- `stateVisualizer.node.classes`: (function) A function that returns classnames to apply to a node

#### `stateVisualizer.node.label`

The labels for tree nodes can be customized.

Provide a function that accepts the node object and the default label and returns a string:

```
function(node, defaultLabel) { return "label"; }
```

This example adds ` (future)` to future states.
_Note: `node.self` contains a reference to the state declaration object._

```js
var options = {
stateVisualizer: {
node: {
label: function (node, defaultLabel) {
return node.self.name.endsWith('.**') ? defaultLabel + ' (future)' : defaultLabel;
},
},
},
};

The state tree visualizer can be configured to style each node specifically.
var pluginInstance = uiRouterInstance.plugin(Visualizer, options);
```

Example below marks every node with angular.js view with is-ng1 class.
#### `stateVisualizer.node.classes`

The state tree visualizer can be configured to add additional classes to nodes.
Example below marks every node with angular.js view with `is-ng1` class.

```js
var options = {
stateVisualizer: {
node: {
classes(node) {
return Object.entries(node.views || {}).some(routeView => routeView[1] && routeView[1].$type === 'ng1')
return Object.entries(node.views || {}).some((routeView) => routeView[1] && routeView[1].$type === 'ng1')
? 'is-ng1'
: '';
},
Expand All @@ -109,8 +144,8 @@ Inject the `$uiRouter` router instance in a run block.

```js
// inject the router instance into a `run` block by name
app.run(function($uiRouter) {
var pluginInstance = $uiRouter.plugin(Visualizer);
app.run(function ($uiRouter) {
var pluginInstance = $uiRouter.plugin(Visualizer);
});
```

Expand All @@ -124,7 +159,7 @@ import { Visualizer } from "@uirouter/visualizer";

...

export function configRouter(router: UIRouter) {
export function configRouter(router: UIRouter) {
var pluginInstance = router.plugin(Visualizer);
}

Expand All @@ -137,7 +172,6 @@ export function configRouter(router: UIRouter) {
#### React (Imperative)
Create the UI-Router instance manually by calling `new UIRouterReact();`
```js
Expand All @@ -147,7 +181,7 @@ var pluginInstance = router.plugin(Visualizer);
```
#### React (Declarative)
Add the plugin to your `UIRouter` component
```js
Expand Down
3 changes: 2 additions & 1 deletion src/statevis/interface.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { NodeOptions } from './tree/StateTree';
import { StateVisNode } from './tree/stateVisNode';

export interface NodeDimensions {
Expand All @@ -19,7 +20,7 @@ export interface Renderer {
// Applies a layout to the nodes
layoutFn(rootNode: StateVisNode): void;
// Renders a state label
labelRenderFn(x: number, y: number, node: StateVisNode, renderer: Renderer): any;
labelRenderFn(x: number, y: number, node: StateVisNode, nodeOptions: NodeOptions, renderer: Renderer): any;
// Renders an edge
edgeRenderFn(rootNode: StateVisNode, renderer: Renderer): any;

Expand Down
34 changes: 19 additions & 15 deletions src/statevis/renderers.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { h } from 'preact';
import { Renderer } from './interface';
import { hierarchy, cluster as d3cluster, tree as d3tree, HierarchyPointNode } from 'd3-hierarchy';
import { NodeOptions } from './tree/StateTree';
import { StateVisNode } from './tree/stateVisNode'; // has or is using

export const RENDERER_PRESETS = {
export const RENDERER_PRESETS: { [name: string]: Partial<Renderer> } = {
Tree: {
layoutFn: TREE_LAYOUT,
sortNodesFn: TOP_TO_BOTTOM_SORT,
Expand Down Expand Up @@ -75,7 +76,7 @@ export function CLUSTER_LAYOUT(rootNode: StateVisNode) {

/** For RADIAL_LAYOUT: projects x/y coords from a cluster layout to circular layout */
function project(x, y) {
let angle = (x - 90) / 180 * Math.PI,
let angle = ((x - 90) / 180) * Math.PI,
radius = y;
const CENTER = 0.5;
return { x: CENTER + radius * Math.cos(angle), y: CENTER + radius * Math.sin(angle) };
Expand All @@ -86,13 +87,13 @@ export function RADIAL_LAYOUT(rootNode: StateVisNode) {

let layout = d3cluster<StateVisNode>()
.size([360, 0.4])
.separation(function(a, b) {
.separation(function (a, b) {
return (a.parent == b.parent ? 1 : 2) / a.depth;
});

let nodes = layout(root);

nodes.each(function(node) {
nodes.each(function (node) {
let projected = project(node.x, node.y);
let visNode: StateVisNode = node.data;
visNode.layoutX = node.x;
Expand All @@ -104,7 +105,7 @@ export function RADIAL_LAYOUT(rootNode: StateVisNode) {

/** Mutates each StateVisNode by copying the new x/y values from the d3 HierarchyPointNode structure */
function updateNodes(nodes: HierarchyPointNode<StateVisNode>) {
nodes.each(node => {
nodes.each((node) => {
node.data.layoutX = node.data.x = node.x;
node.data.layoutY = node.data.y = node.y;
});
Expand All @@ -115,13 +116,11 @@ function updateNodes(nodes: HierarchyPointNode<StateVisNode>) {
// STATE NAME LABEL
///////////////////////////////////////////

export function RADIAL_TEXT(x, y, node: StateVisNode, renderer: Renderer) {
export function RADIAL_TEXT(x, y, node: StateVisNode, nodeOptions: NodeOptions, renderer: Renderer) {
let { baseFontSize, zoom } = renderer;
let fontSize = baseFontSize * zoom;

let segments = node.name.split('.');
let name = segments.pop();
if (name == '**') name = segments.pop() + '.**';
const label = nodeOptions?.label ? nodeOptions.label(node, defaultLabel(node)) : defaultLabel(node);

let angle = node.layoutX || 0;

Expand All @@ -134,25 +133,30 @@ export function RADIAL_TEXT(x, y, node: StateVisNode, renderer: Renderer) {
return (
<text className="name" text-anchor={textAnchor} transform={transform} font-size={fontSize}>
{' '}
{name}{' '}
{label}{' '}
</text>
);
}

export function SLANTED_TEXT(x, y, node: StateVisNode, renderer: Renderer) {
let { baseRadius, baseFontSize, baseStrokeWidth, baseNodeStrokeWidth, zoom } = renderer;
let r = baseRadius * zoom;
let fontSize = baseFontSize * zoom;
export function defaultLabel(node: StateVisNode) {
let segments = node.name.split('.');
let name = segments.pop();
if (name == '**') name = segments.pop() + '.**';
return name;
}

export function SLANTED_TEXT(x, y, node: StateVisNode, nodeOptions: NodeOptions, renderer: Renderer) {
let { baseFontSize, zoom } = renderer;
let fontSize = baseFontSize * zoom;

const label = nodeOptions?.label ? nodeOptions.label(node, defaultLabel(node)) : defaultLabel(node);

let transform = `rotate(-15),translate(0, ${-15 * zoom})`;

return (
<text className="name" text-anchor="middle" transform={transform} font-size={fontSize}>
{' '}
{name}{' '}
{label}{' '}
</text>
);
}
Expand Down
2 changes: 1 addition & 1 deletion src/statevis/tree/StateNode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export class StateNode extends Component<IProps, IState> {
</text>
)}

{renderer.labelRenderFn(x, y, node, renderer)}
{renderer.labelRenderFn(x, y, node, nodeOptions, renderer)}

<text className="label" text-anchor="middle" font-size={fontSize} transform={`translate(0, ${r * 2})`}>
{node.label}
Expand Down
Loading

0 comments on commit d3cfd6e

Please sign in to comment.