This is an alpha version of a finite state machine and statechart visualizer based on my XState library and Cytoscape.
A Pen by Joshua Levine on CodePen.
| <div id="app"></div> | |
| <!-- <div id="cy"></div> --> |
| const pedestrianStates = { | |
| initial: 'walk', | |
| states: { | |
| walk: { | |
| on: { | |
| PED_TIMER: 'wait' | |
| } | |
| }, | |
| wait: { | |
| on: { | |
| PED_TIMER: 'stop' | |
| } | |
| }, | |
| stop: {} | |
| } | |
| }; | |
| const lightMachine = { | |
| id: 'light', | |
| initial: 'green', | |
| states: { | |
| green: { | |
| on: { | |
| TIMER: 'yellow' | |
| } | |
| }, | |
| yellow: { | |
| on: { | |
| TIMER: 'red' | |
| } | |
| }, | |
| red: { | |
| on: { | |
| TIMER: 'green' | |
| }, | |
| ...pedestrianStates | |
| } | |
| } | |
| }; | |
| class Graph extends React.Component { | |
| constructor() { | |
| super(); | |
| this.state = { | |
| nodes: [], | |
| edges: [], | |
| raw: JSON.stringify(lightMachine, null, 2), | |
| machine: lightMachine | |
| } | |
| } | |
| initializeMachine() { | |
| const { machine } = this.state; | |
| const nodes = []; | |
| const edges = []; | |
| function addNodesAndEdges(node, key, parent) { | |
| const id = parent ? parent + '.' + key : key; | |
| if (parent) { | |
| nodes.push({ | |
| data: { | |
| id, | |
| label: key, | |
| parent | |
| } | |
| }); | |
| } | |
| if (node.states) { | |
| const states = Object.keys(node.states) | |
| .map(key => ({ | |
| ...node.states[key], | |
| id: key | |
| })) | |
| .concat({ | |
| id: '$initial', | |
| initial: 1, | |
| on: {'': node.initial} | |
| }); | |
| states.forEach(state => { | |
| addNodesAndEdges(state, state.id, id) | |
| }); | |
| } | |
| if (node.on) { | |
| const visited = {}; | |
| Object.keys(node.on).forEach(event => { | |
| const target = node.on[event]; | |
| (visited[target] || (visited[target] = [])).push(event); | |
| }); | |
| Object.keys(visited).forEach(target => { | |
| edges.push({ | |
| data: { | |
| id: key + ':' + target, | |
| source: id, | |
| target: parent ? parent + '.' + target : target, | |
| label: visited[target].join(',\n'), | |
| } | |
| }); | |
| }); | |
| } | |
| } | |
| addNodesAndEdges(machine, machine.id || 'machine'); | |
| this.cy = cytoscape({ | |
| container: this.cyNode, | |
| boxSelectionEnabled: true, | |
| autounselectify: true, | |
| style: ` | |
| node[label != '$initial'] { | |
| content: data(label); | |
| text-valign: center; | |
| text-halign: center; | |
| shape: roundrectangle; | |
| width: label; | |
| height: label; | |
| padding-left: 5px; | |
| padding-right: 5px; | |
| padding-top: 5px; | |
| padding-bottom: 5px; | |
| background-color: white; | |
| border-width: 1px; | |
| border-color: black; | |
| font-size: 10px; | |
| font-family: Helvetica Neue; | |
| } | |
| node:active { | |
| overlay-color: black; | |
| overlay-padding: 0; | |
| overlay-opacity: 0.1; | |
| } | |
| .foo { | |
| background-color: blue; | |
| } | |
| node[label = '$initial'] { | |
| visibility: hidden; | |
| } | |
| $node > node { | |
| padding-top: 1px; | |
| padding-left: 10px; | |
| padding-bottom: 10px; | |
| padding-right: 10px; | |
| text-valign: top; | |
| text-halign: center; | |
| border-width: 1px; | |
| border-color: black; | |
| background-color: white; | |
| } | |
| edge { | |
| curve-style: bezier; | |
| width: 1px; | |
| target-arrow-shape: triangle; | |
| label: data(label); | |
| font-size: 5px; | |
| font-weight: bold; | |
| text-background-color: #fff; | |
| text-background-padding: 3px; | |
| line-color: black; | |
| target-arrow-color: black; | |
| z-index: 100; | |
| text-wrap: wrap; | |
| text-background-color: white; | |
| text-background-opacity: 1; | |
| target-distance-from-node: 2px; | |
| } | |
| edge[label = ''] { | |
| source-arrow-shape: circle; | |
| source-arrow-color: black; | |
| } | |
| `, | |
| elements: { | |
| nodes, | |
| edges | |
| }, | |
| layout: { | |
| name: 'cose-bilkent', | |
| randomize: true, | |
| idealEdgeLength: 70, | |
| animate: false | |
| } | |
| }); | |
| } | |
| componentDidMount() { | |
| this.initializeMachine(); | |
| } | |
| handleChange(raw) { | |
| this.setState({ raw }); | |
| } | |
| generateGraph() { | |
| try { | |
| // be a little lax. | |
| const machine = eval(`var r=${this.state.raw};r`) | |
| this.setState({ machine, error: false }, this.initializeMachine) | |
| } catch(e) { | |
| console.error(e); | |
| this.setState({ error: true }); | |
| } | |
| } | |
| render() { | |
| return <div className="container"> | |
| <div className="editor"> | |
| <textarea | |
| value={this.state.raw} | |
| onChange={e => this.handleChange(e.target.value)} | |
| /> | |
| <button onClick={() => this.generateGraph()}>Generate graph</button> | |
| </div> | |
| <div id="cy" ref={n => this.cyNode = n} /> | |
| </div> | |
| } | |
| } | |
| ReactDOM.render(<Graph />, document.querySelector('#app')); |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/cytoscape/3.1.4/cytoscape.min.js"></script> | |
| <script src="https://cdn.rawgit.com/cytoscape/cytoscape.js-cose-bilkent/1.6.5/cytoscape-cose-bilkent.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.6.1/react.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.6.1/react-dom.min.js"></script> |
This is an alpha version of a finite state machine and statechart visualizer based on my XState library and Cytoscape.
A Pen by Joshua Levine on CodePen.
| * { | |
| box-sizing: border-box; | |
| position: relative; | |
| } | |
| html, body { | |
| height: 100%; | |
| width: 100%; | |
| margin: 0; | |
| } | |
| .container { | |
| display: flex; | |
| flex-direction: row; | |
| align-items: stretch; | |
| height: 100vh; | |
| width: 100vw; | |
| } | |
| .editor { | |
| display: flex; | |
| flex-direction: column; | |
| flex-basis: 33%; | |
| > textarea { | |
| flex-grow: 1; | |
| font-family: Monaco, monospace; | |
| font-size: 16px; | |
| background: #161818; | |
| color: #FEFEFE; | |
| line-height: 1.2; | |
| } | |
| } | |
| button { | |
| -webkit-appearance: none; | |
| padding: 1rem; | |
| background: blue; | |
| color: white; | |
| text-transform: uppercase; | |
| letter-spacing: 1px; | |
| font-size: 12px; | |
| border: none; | |
| background-color: #FF3CAC; | |
| background-image: linear-gradient(135deg, #FF3CAC 0%, #784BA0 50%, #2B86C5 100%); | |
| transition: opacity ease-out 0.3s; | |
| cursor: pointer; | |
| &:hover { | |
| opacity: 0.8; | |
| } | |
| } | |
| #cy { | |
| // width: 50vw; | |
| height: 100vh; | |
| flex-grow: 1; | |
| } |