Quellcode durchsuchen

Update view-grapher.js (#68)

Lutz Roeder vor 4 Jahren
Ursprung
Commit
215fe220b2
2 geänderte Dateien mit 355 neuen und 334 gelöschten Zeilen
  1. 217 210
      source/view-grapher.js
  2. 138 124
      source/view.js

+ 217 - 210
source/view-grapher.js

@@ -99,16 +99,12 @@ grapher.Graph = class {
         }
     }
 
-    build(document, originElement) {
-
-        const createElement = (name) => {
-            return document.createElementNS('http://www.w3.org/2000/svg', name);
-        };
+    build(document, origin) {
         const createGroup = (name) => {
-            const element = createElement('g');
+            const element = document.createElementNS('http://www.w3.org/2000/svg', 'g');
             element.setAttribute('id', name);
             element.setAttribute('class', name);
-            originElement.appendChild(element);
+            origin.appendChild(element);
             return element;
         };
 
@@ -117,10 +113,10 @@ grapher.Graph = class {
         const edgeLabelGroup = createGroup('edge-labels');
         const nodeGroup = createGroup('nodes');
 
-        const edgePathGroupDefs = createElement('defs');
+        const edgePathGroupDefs = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
         edgePathGroup.appendChild(edgePathGroupDefs);
         const marker = (id) => {
-            const element = createElement('marker');
+            const element = document.createElementNS('http://www.w3.org/2000/svg', 'marker');
             element.setAttribute('id', id);
             element.setAttribute('viewBox', '0 0 10 10');
             element.setAttribute('refX', 9);
@@ -129,7 +125,7 @@ grapher.Graph = class {
             element.setAttribute('markerWidth', 8);
             element.setAttribute('markerHeight', 6);
             element.setAttribute('orient', 'auto');
-            const markerPath = createElement('path');
+            const markerPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
             markerPath.setAttribute('d', 'M 0 0 L 10 5 L 0 10 L 4 5 z');
             markerPath.style.setProperty('stroke-width', 1);
             element.appendChild(markerPath);
@@ -146,14 +142,14 @@ grapher.Graph = class {
             }
             else {
                 // cluster
-                node.rectangle = createElement('rect');
+                node.rectangle = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
                 if (node.rx) {
                     node.rectangle.setAttribute('rx', node.rx);
                 }
                 if (node.ry) {
                     node.rectangle.setAttribute('ry', node.ry);
                 }
-                node.element = createElement('g');
+                node.element = document.createElementNS('http://www.w3.org/2000/svg', 'g');
                 node.element.setAttribute('class', 'cluster');
                 node.element.appendChild(node.rectangle);
                 clusterGroup.appendChild(node.element);
@@ -165,15 +161,13 @@ grapher.Graph = class {
         }
     }
 
-    layout() {
-
+    update() {
         dagre.layout(this);
-
         for (const nodeId of this.nodes().keys()) {
             const node = this.node(nodeId);
             if (this.children(nodeId).length == 0) {
                 // node
-                node.layout();
+                node.update();
             }
             else {
                 // cluster
@@ -185,9 +179,8 @@ grapher.Graph = class {
                 node.rectangle.setAttribute('height', node.height);
             }
         }
-
         for (const edge of this.edges().values()) {
-            edge.label.layout();
+            edge.label.update();
         }
     }
 };
@@ -210,47 +203,49 @@ grapher.Node = class {
         return block;
     }
 
-    build(document, contextElement) {
-        const createElement = (name) => {
-            return document.createElementNS('http://www.w3.org/2000/svg', name);
-        };
-        this.element = createElement('g');
+    canvas() {
+        const block = new grapher.Node.Canvas();
+        this._blocks.push(block);
+        return block;
+    }
+
+    build(document, parent) {
+        this.element = document.createElementNS('http://www.w3.org/2000/svg', 'g');
         if (this.id) {
             this.element.setAttribute('id', this.id);
         }
         this.element.setAttribute('class', this.class ? 'node ' + this.class : 'node');
         this.element.style.opacity = 0;
-
-        contextElement.appendChild(this.element);
-
-        let width = 0;
-        let height = 0;
-        const tops = [];
-
-        for (const block of this._blocks) {
-            tops.push(height);
+        parent.appendChild(this.element);
+        this.border = document.createElementNS('http://www.w3.org/2000/svg', 'path');
+        this.border.setAttribute('class', [ 'node', 'border' ].join(' '));
+        for (let i = 0; i < this._blocks.length; i++) {
+            const block = this._blocks[i];
+            block.first = i === 0;
+            block.last = i === this._blocks.length - 1;
             block.build(document, this.element);
-            if (width < block.width) {
-                width = block.width;
-            }
-            height = height + block.height;
         }
+        this.element.appendChild(this.border);
+        this.layout();
+    }
 
+    layout() {
+        const width = Math.max(...this._blocks.map((block) => block.width));
+        let height = 0;
         for (let i = 0; i < this._blocks.length; i++) {
-            const top = tops.shift();
-            this._blocks[i].update(this.element, top, width, i == 0, i == this._blocks.length - 1);
+            const block = this._blocks[i];
+            block.y = height;
+            // block.width = width;
+            block.update(this.element, height, width, i == 0, i == this._blocks.length - 1);
+            height = height + block.height;
         }
-
-        const borderElement = createElement('path');
-        borderElement.setAttribute('class', [ 'node', 'border' ].join(' '));
-        borderElement.setAttribute('d', grapher.Node.roundedRect(0, 0, width, height, true, true, true, true));
-        this.element.appendChild(borderElement);
+        this.border.setAttribute('d', grapher.Node.roundedRect(0, 0, width, height, true, true, true, true));
         const nodeBox = this.element.getBBox();
         this.width = nodeBox.width;
         this.height = nodeBox.height;
     }
 
-    layout() {
+    update() {
         this.element.setAttribute('transform', 'translate(' + (this.x - (this.width / 2)) + ',' + (this.y - (this.height / 2)) + ')');
         this.element.style.opacity = 1;
     }
@@ -277,140 +272,138 @@ grapher.Node = class {
 grapher.Node.Header = class {
 
     constructor() {
-        this._items = [];
+        this._entries = [];
     }
 
     add(id, classList, content, tooltip, handler) {
-        this._items.push({
-            id: id,
-            classList: classList,
-            content: content,
-            tooltip: tooltip,
-            handler: handler
-        });
+        const entry = new grapher.Node.Header.Entry(id, classList, content, tooltip, handler);
+        this._entries.push(entry);
+        return entry;
     }
 
-    build(document, parentElement) {
+    build(document, parent) {
         this._document = document;
-        this._width = 0;
-        this._height = 0;
-        this._elements = [];
+        this.width = 0;
+        this.height = 0;
         let x = 0;
         const y = 0;
-        for (const item of this._items) {
-            const yPadding = 4;
-            const xPadding = 7;
-            const element = this.createElement('g');
-            const classList = [ 'node-item' ];
-            parentElement.appendChild(element);
-            const pathElement = this.createElement('path');
-            const textElement = this.createElement('text');
-            element.appendChild(pathElement);
-            element.appendChild(textElement);
-            if (item.classList) {
-                classList.push(...item.classList);
-            }
-            element.setAttribute('class', classList.join(' '));
-            if (item.id) {
-                element.setAttribute('id', item.id);
-            }
-            if (item.handler) {
-                element.addEventListener('click', item.handler);
-            }
-            if (item.tooltip) {
-                const titleElement = this.createElement('title');
-                titleElement.textContent = item.tooltip;
-                element.appendChild(titleElement);
-            }
-            if (item.content) {
-                textElement.textContent = item.content;
+        for (const entry of this._entries) {
+            entry.x = x;
+            entry.y = y;
+            entry.build(document, parent);
+            x += entry.width;
+            this.height = Math.max(entry.height, this.height);
+            this.width = Math.max(x, this.width);
+        }
+        if (!this.first) {
+            this.line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
+            parent.appendChild(this.line);
+        }
+    }
+
+    update(parent, top, width) {
+        const dx = width - this.width;
+        for (let i = 0; i < this._entries.length; i++) {
+            const entry = this._entries[i];
+            if (i == 0) {
+                entry.width = entry.width + dx;
             }
-            const boundingBox = textElement.getBBox();
-            const width = boundingBox.width + xPadding + xPadding;
-            const height = boundingBox.height + yPadding + yPadding;
-            this._elements.push({
-                'group': element,
-                'text': textElement,
-                'path': pathElement,
-                'x': x, 'y': y,
-                'width': width, 'height': height,
-                'tx': xPadding, 'ty': yPadding - boundingBox.y,
-            });
-            x += width;
-            if (this._height < height) {
-                this._height = height;
+            else {
+                entry.x = entry.x + dx;
+                entry.tx = entry.tx + dx;
             }
-            if (x > this._width) {
-                this._width = x;
+            entry.y = entry.y + top;
+        }
+        for (let i = 0; i < this._entries.length; i++) {
+            const entry = this._entries[i];
+            entry.element.setAttribute('transform', 'translate(' + entry.x + ',' + entry.y + ')');
+            const r1 = i == 0 && this.first;
+            const r2 = i == this._entries.length - 1 && this.first;
+            const r3 = i == this._entries.length - 1 && this.last;
+            const r4 = i == 0 && this.last;
+            entry.path.setAttribute('d', grapher.Node.roundedRect(0, 0, entry.width, entry.height, r1, r2, r3, r4));
+            entry.text.setAttribute('x', 6);
+            entry.text.setAttribute('y', entry.ty);
+        }
+        for (let i = 0; i < this._entries.length; i++) {
+            const entry = this._entries[i];
+            if (i != 0) {
+                const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
+                line.setAttribute('class', 'node');
+                line.setAttribute('x1', entry.x);
+                line.setAttribute('x2', entry.x);
+                line.setAttribute('y1', top);
+                line.setAttribute('y2', top + this.height);
+                parent.appendChild(line);
             }
         }
+        if (this.line) {
+            this.line.setAttribute('class', 'node');
+            this.line.setAttribute('x1', 0);
+            this.line.setAttribute('x2', width);
+            this.line.setAttribute('y1', top);
+            this.line.setAttribute('y2', top);
+        }
     }
+};
 
-    get width() {
-        return this._width;
-    }
+grapher.Node.Header.Entry = class {
 
-    get height() {
-        return this._height;
+    constructor(id, classList, content, tooltip, handler) {
+        this.id = id;
+        this.classList = classList;
+        this.content = content;
+        this.tooltip = tooltip;
+        this.handler = handler;
+        this.events = {};
     }
 
-    update(parentElement, top, width, first, last) {
-
-        const dx = width - this._width;
-        let i;
-        let element;
+    on(event, callback) {
+        this.events[event] = this.events[event] || [];
+        this.events[event].push(callback);
+    }
 
-        for (i = 0; i < this._elements.length; i++) {
-            element = this._elements[i];
-            if (i == 0) {
-                element.width = element.width + dx;
+    raise(event, data) {
+        if (this.events && this.events[event]) {
+            for (const callback of this.events[event]) {
+                callback(this, data);
             }
-            else {
-                element.x = element.x + dx;
-                element.tx = element.tx + dx;
-            }
-            element.y = element.y + top;
         }
+    }
 
-        for (i = 0; i < this._elements.length; i++) {
-            element = this._elements[i];
-            element.group.setAttribute('transform', 'translate(' + element.x + ',' + element.y + ')');
-            const r1 = i == 0 && first;
-            const r2 = i == this._elements.length - 1 && first;
-            const r3 = i == this._elements.length - 1 && last;
-            const r4 = i == 0 && last;
-            element.path.setAttribute('d', grapher.Node.roundedRect(0, 0, element.width, element.height, r1, r2, r3, r4));
-            element.text.setAttribute('x', 6);
-            element.text.setAttribute('y', element.ty);
+    build(document, parent) {
+        const yPadding = 4;
+        const xPadding = 7;
+        this.element = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+        parent.appendChild(this.element);
+        this.path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
+        this.text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
+        this.element.appendChild(this.path);
+        this.element.appendChild(this.text);
+        const classList = [ 'node-item' ];
+        if (this.classList) {
+            classList.push(...this.classList);
         }
-
-        let lineElement;
-        for (i = 0; i < this._elements.length; i++) {
-            element = this._elements[i];
-            if (i != 0) {
-                lineElement = this.createElement('line');
-                lineElement.setAttribute('class', 'node');
-                lineElement.setAttribute('x1', element.x);
-                lineElement.setAttribute('x2', element.x);
-                lineElement.setAttribute('y1', top);
-                lineElement.setAttribute('y2', top + this._height);
-                parentElement.appendChild(lineElement);
-            }
+        this.element.setAttribute('class', classList.join(' '));
+        if (this.id) {
+            this.element.setAttribute('id', this.id);
         }
-
-        if (!first) {
-            lineElement = this.createElement('line');
-            lineElement.setAttribute('class', 'node');
-            lineElement.setAttribute('x1', 0);
-            lineElement.setAttribute('x2', width);
-            lineElement.setAttribute('y1', top);
-            lineElement.setAttribute('y2', top);
-            parentElement.appendChild(lineElement);
+        if (this.events.click) {
+            this.element.addEventListener('click', () => this.raise('click'));
         }
-    }
-
-    createElement(name) {
-        return this._document.createElementNS('http://www.w3.org/2000/svg', name);
+        if (this.tooltip) {
+            const titleElement = document.createElementNS('http://www.w3.org/2000/svg', 'title');
+            titleElement.textContent = this.tooltip;
+            this.element.appendChild(titleElement);
+        }
+        if (this.content) {
+            this.text.textContent = this.content;
+        }
+        const boundingBox = this.text.getBBox();
+        this.width = boundingBox.width + xPadding + xPadding;
+        this.height = boundingBox.height + yPadding + yPadding;
+        this.tx = xPadding;
+        this.ty = yPadding - boundingBox.y;
     }
 };
 
@@ -418,103 +411,117 @@ grapher.Node.List = class {
 
     constructor() {
         this._items = [];
+        this.events = {};
     }
 
     add(id, name, value, tooltip, separator) {
-        this._items.push({ id: id, name: name, value: value, tooltip: tooltip, separator: separator });
+        const item = new grapher.Node.List.Item(id, name, value, tooltip, separator);
+        this._items.push(item);
+        return item;
     }
 
-    get handler() {
-        return this._handler;
+    on(event, callback) {
+        this.events[event] = this.events[event] || [];
+        this.events[event].push(callback);
     }
 
-    set handler(handler) {
-        this._handler = handler;
+    raise(event, data) {
+        if (this.events && this.events[event]) {
+            for (const callback of this.events[event]) {
+                callback(this, data);
+            }
+        }
     }
 
-    build(document, parentElement) {
+    build(document, parent) {
         this._document = document;
-        this._width = 0;
-        this._height = 0;
+        this.width = 0;
+        this.height = 0;
         const x = 0;
         const y = 0;
-        this._element = this.createElement('g');
-        this._element.setAttribute('class', 'node-attribute');
-        parentElement.appendChild(this._element);
-        if (this._handler) {
-            this._element.addEventListener('click', this._handler);
-        }
-        this._backgroundElement = this.createElement('path');
-        this._element.appendChild(this._backgroundElement);
-        this._element.setAttribute('transform', 'translate(' + x + ',' + y + ')');
-        this._height += 3;
+        this.element = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+        this.element.setAttribute('class', 'node-attribute');
+        if (this.events.click) {
+            this.element.addEventListener('click', () => this.raise('click'));
+        }
+        this.element.setAttribute('transform', 'translate(' + x + ',' + y + ')');
+        this.background = document.createElementNS('http://www.w3.org/2000/svg', 'path');
+        this.element.appendChild(this.background);
+        parent.appendChild(this.element);
+        this.height += 3;
         for (const item of this._items) {
             const yPadding = 1;
             const xPadding = 6;
-            const textElement = this.createElement('text');
+            const textElement = document.createElementNS('http://www.w3.org/2000/svg', 'text');
             if (item.id) {
                 textElement.setAttribute('id', item.id);
             }
             textElement.setAttribute('xml:space', 'preserve');
-            this._element.appendChild(textElement);
+            this.element.appendChild(textElement);
             if (item.tooltip) {
-                const titleElement = this.createElement('title');
-                titleElement.textContent = item.tooltip;
-                textElement.appendChild(titleElement);
+                const title = document.createElementNS('http://www.w3.org/2000/svg', 'title');
+                title.textContent = item.tooltip;
+                textElement.appendChild(title);
             }
-            const textNameElement = this.createElement('tspan');
+            const textNameElement = document.createElementNS('http://www.w3.org/2000/svg', 'tspan');
             textNameElement.textContent = item.name;
             if (item.separator.trim() != '=') {
                 textNameElement.style.fontWeight = 'bold';
             }
             textElement.appendChild(textNameElement);
-            const textValueElement = this.createElement('tspan');
+            const textValueElement = document.createElementNS('http://www.w3.org/2000/svg', 'tspan');
             textValueElement.textContent = item.separator + item.value;
             textElement.appendChild(textValueElement);
             const size = textElement.getBBox();
             const width = xPadding + size.width + xPadding;
-            if (this._width < width) {
-                this._width = width;
-            }
+            this.width = Math.max(width, this.width);
             textElement.setAttribute('x', x + xPadding);
-            textElement.setAttribute('y', this._height + yPadding - size.y);
-            this._height += yPadding + size.height + yPadding;
+            textElement.setAttribute('y', this.height + yPadding - size.y);
+            this.height += yPadding + size.height + yPadding;
         }
-        this._height += 3;
+        this.height += 3;
+        this.width = Math.max(75, this.width);
+        if (!this.first) {
+            this.line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
+            this.line.setAttribute('class', 'node');
+            this.element.appendChild(this.line);
+        }
+    }
 
-        if (this._width < 75) {
-            this._width = 75;
+    update(parent, top, width) {
+        this.element.setAttribute('transform', 'translate(0,' + this.y + ')');
+        this.background.setAttribute('d', grapher.Node.roundedRect(0, 0, width, this.height, this.first, this.first, this.last, this.last));
+        if (this.line) {
+            this.line.setAttribute('x1', 0);
+            this.line.setAttribute('x2', width);
+            this.line.setAttribute('y1', 0);
+            this.line.setAttribute('y2', 0);
         }
     }
+};
 
-    get width() {
-        return this._width;
+grapher.Node.List.Item = class {
+
+    constructor(id, name, value, tooltip, separator) {
+        this.id = id;
+        this.name = name;
+        this.value = value;
+        this.tooltip = tooltip;
+        this.separator = separator;
     }
+};
 
-    get height() {
-        return this._height;
+grapher.Node.Canvas = class {
+
+    constructor() {
+        this.width = 0;
+        this.height = 0;
     }
 
-    update(parentElement, top, width , first, last) {
-        this._element.setAttribute('transform', 'translate(0,' + top + ')');
-        const r1 = first;
-        const r2 = first;
-        const r3 = last;
-        const r4 = last;
-        this._backgroundElement.setAttribute('d', grapher.Node.roundedRect(0, 0, width, this._height, r1, r2, r3, r4));
-        if (!first) {
-            const line = this.createElement('line');
-            line.setAttribute('class', 'node');
-            line.setAttribute('x1', 0);
-            line.setAttribute('x2', width);
-            line.setAttribute('y1', 0);
-            line.setAttribute('y2', 0);
-            this._element.appendChild(line);
-        }
+    build(/* document, parent */) {
     }
 
-    createElement(name) {
-        return this._document.createElementNS('http://www.w3.org/2000/svg', name);
+    update(/* parent, top, width , first, last */) {
     }
 };
 
@@ -559,7 +566,7 @@ grapher.Edge = class {
         }
     }
 
-    layout() {
+    update() {
         const edgePath = grapher.Edge._computeCurvePath(this, this.from, this.to);
         this.element.setAttribute('d', edgePath);
         if (this.labelElement) {

+ 138 - 124
source/view.js

@@ -555,107 +555,7 @@ view.View = class {
                 }
 
                 const viewGraph = new view.Graph(this, model, groups, options);
-
-                const clusters = new Set();
-                const clusterParentMap = new Map();
-
-                if (groups) {
-                    for (const node of nodes) {
-                        if (node.group) {
-                            const path = node.group.split('/');
-                            while (path.length > 0) {
-                                const name = path.join('/');
-                                path.pop();
-                                clusterParentMap.set(name, path.join('/'));
-                            }
-                        }
-                    }
-                }
-
-                for (const input of graph.inputs) {
-                    const viewInput = viewGraph.createInput(input);
-                    for (const argument of input.arguments) {
-                        viewGraph.createArgument(argument).from(viewInput);
-                    }
-                }
-
-                for (const node of nodes) {
-
-                    const viewNode = viewGraph.createNode(node);
-
-                    const inputs = node.inputs;
-                    for (const input of inputs) {
-                        for (const argument of input.arguments) {
-                            if (argument.name != '' && !argument.initializer) {
-                                viewGraph.createArgument(argument).to(viewNode);
-                            }
-                        }
-                    }
-                    let outputs = node.outputs;
-                    if (node.chain && node.chain.length > 0) {
-                        const chainOutputs = node.chain[node.chain.length - 1].outputs;
-                        if (chainOutputs.length > 0) {
-                            outputs = chainOutputs;
-                        }
-                    }
-                    for (const output of outputs) {
-                        for (const argument of output.arguments) {
-                            if (!argument) {
-                                throw new view.Error("Invalid null argument in '" + model.identifier + "'.");
-                            }
-                            if (argument.name != '') {
-                                viewGraph.createArgument(argument).from(viewNode);
-                            }
-                        }
-                    }
-
-                    if (node.controlDependencies && node.controlDependencies.length > 0) {
-                        for (const argument of node.controlDependencies) {
-                            viewGraph.createArgument(argument).to(viewNode, true);
-                        }
-                    }
-
-                    const createCluster = function(name) {
-                        if (!clusters.has(name)) {
-                            viewGraph.setNode({ name: name, rx: 5, ry: 5});
-                            clusters.add(name);
-                            const parent = clusterParentMap.get(name);
-                            if (parent) {
-                                createCluster(parent);
-                                viewGraph.setParent(name, parent);
-                            }
-                        }
-                    };
-
-                    if (groups) {
-                        let groupName = node.group;
-                        if (groupName && groupName.length > 0) {
-                            if (!clusterParentMap.has(groupName)) {
-                                const lastIndex = groupName.lastIndexOf('/');
-                                if (lastIndex != -1) {
-                                    groupName = groupName.substring(0, lastIndex);
-                                    if (!clusterParentMap.has(groupName)) {
-                                        groupName = null;
-                                    }
-                                }
-                                else {
-                                    groupName = null;
-                                }
-                            }
-                            if (groupName) {
-                                createCluster(groupName);
-                                viewGraph.setParent(viewNode.name, groupName);
-                            }
-                        }
-                    }
-                }
-
-                for (const output of graph.outputs) {
-                    const viewOutput = viewGraph.createOutput(output);
-                    for (const argument of output.arguments) {
-                        viewGraph.createArgument(argument).to(viewOutput);
-                    }
-                }
+                viewGraph.add(graph);
 
                 // Workaround for Safari background drag/zoom issue:
                 // https://stackoverflow.com/questions/40887193/d3-js-zoom-is-not-working-with-mousewheel-in-safari
@@ -675,7 +575,7 @@ view.View = class {
 
                 return this._timeout(20).then(() => {
 
-                    viewGraph.layout();
+                    viewGraph.update();
 
                     const elements = Array.from(canvas.getElementsByClassName('graph-input') || []);
                     if (elements.length === 0) {
@@ -965,13 +865,114 @@ view.Graph = class extends grapher.Graph {
         return value;
     }
 
-    build(document, originElement) {
+    add(graph) {
+        const clusters = new Set();
+        const clusterParentMap = new Map();
+        const groups = graph.groups;
+        if (groups) {
+            for (const node of graph.nodes) {
+                if (node.group) {
+                    const path = node.group.split('/');
+                    while (path.length > 0) {
+                        const name = path.join('/');
+                        path.pop();
+                        clusterParentMap.set(name, path.join('/'));
+                    }
+                }
+            }
+        }
 
+        for (const input of graph.inputs) {
+            const viewInput = this.createInput(input);
+            for (const argument of input.arguments) {
+                this.createArgument(argument).from(viewInput);
+            }
+        }
+
+        for (const node of graph.nodes) {
+
+            const viewNode = this.createNode(node);
+
+            const inputs = node.inputs;
+            for (const input of inputs) {
+                for (const argument of input.arguments) {
+                    if (argument.name != '' && !argument.initializer) {
+                        this.createArgument(argument).to(viewNode);
+                    }
+                }
+            }
+            let outputs = node.outputs;
+            if (node.chain && node.chain.length > 0) {
+                const chainOutputs = node.chain[node.chain.length - 1].outputs;
+                if (chainOutputs.length > 0) {
+                    outputs = chainOutputs;
+                }
+            }
+            for (const output of outputs) {
+                for (const argument of output.arguments) {
+                    if (!argument) {
+                        throw new view.Error("Invalid null argument in '" + this.model.identifier + "'.");
+                    }
+                    if (argument.name != '') {
+                        this.createArgument(argument).from(viewNode);
+                    }
+                }
+            }
+
+            if (node.controlDependencies && node.controlDependencies.length > 0) {
+                for (const argument of node.controlDependencies) {
+                    this.createArgument(argument).to(viewNode, true);
+                }
+            }
+
+            const createCluster = (name) => {
+                if (!clusters.has(name)) {
+                    this.setNode({ name: name, rx: 5, ry: 5});
+                    clusters.add(name);
+                    const parent = clusterParentMap.get(name);
+                    if (parent) {
+                        createCluster(parent);
+                        this.setParent(name, parent);
+                    }
+                }
+            };
+
+            if (groups) {
+                let groupName = node.group;
+                if (groupName && groupName.length > 0) {
+                    if (!clusterParentMap.has(groupName)) {
+                        const lastIndex = groupName.lastIndexOf('/');
+                        if (lastIndex != -1) {
+                            groupName = groupName.substring(0, lastIndex);
+                            if (!clusterParentMap.has(groupName)) {
+                                groupName = null;
+                            }
+                        }
+                        else {
+                            groupName = null;
+                        }
+                    }
+                    if (groupName) {
+                        createCluster(groupName);
+                        this.setParent(viewNode.name, groupName);
+                    }
+                }
+            }
+        }
+
+        for (const output of graph.outputs) {
+            const viewOutput = this.createOutput(output);
+            for (const argument of output.arguments) {
+                this.createArgument(argument).to(viewOutput);
+            }
+        }
+    }
+
+    build(document, origin) {
         for (const argument of this._arguments.values()) {
             argument.build();
         }
-
-        super.build(document, originElement);
+        super.build(document, origin);
     }
 };
 
@@ -999,7 +1000,6 @@ view.Node = class extends grapher.Node {
     }
 
     _add(node) {
-
         const header =  this.header();
         const styles = [ 'node-item-type' ];
         const type = node.type;
@@ -1013,18 +1013,15 @@ view.Node = class extends grapher.Node {
         }
         const content = this.context.view.options.names && (node.name || node.location) ? (node.name || node.location) : type.name.split('.').pop();
         const tooltip = this.context.view.options.names && (node.name || node.location) ? type.name : (node.name || node.location);
-        header.add(null, styles, content, tooltip, () => {
-            this.context.view.showNodeProperties(node, null);
-        });
+        const title = header.add(null, styles, content, tooltip);
+        title.on('click', () => this.context.view.showNodeProperties(node, null));
         if (node.type.nodes) {
-            header.add(null, styles, '\u0192', 'Show Function Definition', () => {
-                this.context.view.pushGraph(node.type);
-            });
+            const definition = header.add(null, styles, '\u0192', 'Show Function Definition');
+            definition.on('click', () => this.context.view.pushGraph(node.type));
         }
         if (node.nodes) {
-            header.add(null, styles, '+', null, () => {
-                // debugger;
-            });
+            // this._expand = header.add(null, styles, '+', null);
+            // this._expand.on('click', () => this.toggle());
         }
         const initializers = [];
         let hiddenInitializers = false;
@@ -1050,10 +1047,8 @@ view.Node = class extends grapher.Node {
             return (au < bu) ? -1 : (au > bu) ? 1 : 0;
         });
         if (initializers.length > 0 || hiddenInitializers || sortedAttributes.length > 0) {
-            const block = this.list();
-            block.handler = () => {
-                this.context.view.showNodeProperties(node);
-            };
+            const list = this.list();
+            list.on('click', () => this.context.view.showNodeProperties(node));
             for (const initializer of initializers) {
                 const argument = initializer.arguments[0];
                 const type = argument.type;
@@ -1082,10 +1077,10 @@ view.Node = class extends grapher.Node {
                         }
                     }
                 }
-                block.add(argument.name ? 'initializer-' + argument.name : '', initializer.name, shape, type ? type.toString() : '', separator);
+                list.add(argument.name ? 'initializer-' + argument.name : '', initializer.name, shape, type ? type.toString() : '', separator);
             }
             if (hiddenInitializers) {
-                block.add(null, '\u3008' + '\u2026' + '\u3009', '', null, '');
+                list.add(null, '\u3008' + '\u2026' + '\u3009', '', null, '');
             }
 
             for (const attribute of sortedAttributes) {
@@ -1094,7 +1089,7 @@ view.Node = class extends grapher.Node {
                     if (value && value.length > 25) {
                         value = value.substring(0, 25) + '\u2026';
                     }
-                    block.add(null, attribute.name, value, attribute.type, ' = ');
+                    list.add(null, attribute.name, value, attribute.type, ' = ');
                 }
             }
         }
@@ -1106,6 +1101,23 @@ view.Node = class extends grapher.Node {
         if (node.inner) {
             this._add(node.inner);
         }
+        if (node.nodes) {
+            this.canvas = this.canvas();
+        }
+    }
+
+    toggle() {
+        this._expand.content = '-';
+        this._graph = new view.Graph(this.context.view, this.context.model, false, {});
+        this._graph.add(this.value);
+        // const document = this.element.ownerDocument;
+        // const parent = this.element.parentElement;
+        // this._graph.build(document, parent);
+        // this._graph.update();
+        this.canvas.width = 300;
+        this.canvas.height = 300;
+        this.layout();
+        this.context.update();
     }
 };
 
@@ -1122,7 +1134,8 @@ view.Input = class extends grapher.Node {
             name = name.split('/').pop();
         }
         const header = this.header();
-        header.add(null, [ 'graph-item-input' ], name, types, () => this.context.view.showModelProperties());
+        const title = header.add(null, [ 'graph-item-input' ], name, types);
+        title.on('click', () => this.context.view.showModelProperties());
         this.id = 'input-' + (name ? 'name-' + name : 'id-' + (view.Input.counter++).toString());
     }
 
@@ -1151,7 +1164,8 @@ view.Output = class extends grapher.Node {
             name = name.split('/').pop();
         }
         const header = this.header();
-        header.add(null, [ 'graph-item-output' ], name, types, () => this.context.view.showModelProperties());
+        const title = header.add(null, [ 'graph-item-output' ], name, types);
+        title.on('click', () => this.context.view.showModelProperties());
     }
 
     get inputs() {