Browse Source

SVG node layout

Lutz Roeder 8 years ago
parent
commit
d5a329a997
8 changed files with 319 additions and 190 deletions
  1. 7 4
      package.json
  2. 2 0
      setup.py
  3. 2 0
      src/view-browser.html
  4. 2 0
      src/view-electron.html
  5. 62 0
      src/view-render.css
  6. 207 0
      src/view-render.js
  7. 1 112
      src/view.css
  8. 36 74
      src/view.js

+ 7 - 4
package.json

@@ -5,7 +5,7 @@
         "email": "[email protected]",
         "url": "lutzroeder.com"
     },
-    "version": "0.4.3",
+    "version": "0.4.5",
     "description": "Viewer for ONNX neural network models",
     "license": "MIT",
     "repository": "lutzroeder/netron",
@@ -35,9 +35,12 @@
         "directories": {
             "buildResources": "setup"
         },
-        "fileAssociations": { 
-            "ext": [ "pb", "onnx" ],
-            "name": "ONNX Model" 
+        "fileAssociations": {
+            "ext": [
+                "pb",
+                "onnx"
+            ],
+            "name": "ONNX Model"
         },
         "mac": {
             "category": "public.app-category.developer-tools",

+ 2 - 0
setup.py

@@ -68,6 +68,8 @@ package_data={
         'view-browser.html',
         'view-browser.js',
         'view-onnx.js',
+        'view-render.css',
+        'view-render.js',
         'view-template.js',
         'view.css',
         'view.js',

+ 2 - 0
src/view-browser.html

@@ -2,6 +2,7 @@
 <head>
 <meta charset='utf-8'>
 <link rel='stylesheet' type='text/css' href='open-sans.css'>
+<link rel='stylesheet' type='text/css' href='view-render.css'>
 <link rel='stylesheet' type='text/css' href='view.css'>
 <script type='text/javascript' src='dagre-d3.js'></script>
 <script type='text/javascript' src='protobuf.js'></script>
@@ -10,6 +11,7 @@
 <script type='text/javascript' src='view-template.js'></script>
 <script type='text/javascript' src='view-browser.js'></script>
 <script type='text/javascript' src='view-onnx.js'></script>
+<script type='text/javascript' src='view-render.js'></script>
 <script type='text/javascript' src='view.js'></script>
 <body>
 <div id='welcome' class='background' style='display: block'>

+ 2 - 0
src/view-electron.html

@@ -2,6 +2,7 @@
 <head>
 <meta charset='utf-8'>
 <link rel='stylesheet' type='text/css' href='../node_modules/npm-font-open-sans/open-sans.css'>
+<link rel='stylesheet' type='text/css' href='view-render.css'>
 <link rel='stylesheet' type='text/css' href='view.css'>
 <script type='text/javascript' src='../node_modules/dagre-d3-renderer/dist/dagre-d3.js'></script>
 <script type='text/javascript' src='../node_modules/protobufjs/dist/protobuf.js'></script>
@@ -10,6 +11,7 @@
 <script type='text/javascript' src='view-template.js'></script>
 <script type='text/javascript' src='view-electron.js'></script>
 <script type='text/javascript' src='view-onnx.js'></script>
+<script type='text/javascript' src='view-render.js'></script>
 <script type='text/javascript' src='view.js'></script>
 <body>
 <div id='welcome' class='background' style='display: block'>

+ 62 - 0
src/view-render.css

@@ -0,0 +1,62 @@
+
+.node-header path { stroke-width: 1px; stroke: #000; fill: #fff; }
+
+.node-header text {
+    font-family: 'Open Sans', --apple-system, "Helvetica Neue", Helvetica, Arial, sans-serf;
+    font-size: 10px;
+    font-weight: 600; 
+    text-rendering: geometricPrecision;
+}
+
+.node-header-operator path { fill: #000; }
+.node-header-operator text { fill: #fff; }
+.node-header-operator:hover { cursor: hand; }
+.node-header-operator:hover path { fill: #fff; }
+.node-header-operator:hover text { fill: #000; }
+.node-header-input path { fill: #eee; }
+.node-header-input:hover { cursor: hand; }
+.node-header-input:hover path { fill: #fff; }
+
+.node-property:hover {
+    cursor: hand;
+}
+
+.node-property text {
+    font-family: 'Open Sans', --apple-system, "Helvetica Neue", Helvetica, Arial, sans-serf;
+    font-size: 7px;
+    font-weight: normal;
+    text-rendering: geometricPrecision;
+}
+
+.node-property path { fill: #fff; stroke-width: 1px; stroke: #000; }
+.node-property:hover path { fill: #f6f6f6; }
+
+.node-attribute:hover {
+    cursor: hand;
+}
+
+.node-attribute text {
+    font-family: 'Open Sans', --apple-system, "Helvetica Neue", Helvetica, Arial, sans-serf;
+    font-size: 7px;
+    font-weight: normal;
+    text-rendering: geometricPrecision;
+}
+
+.node-attribute path { fill: #fff; stroke-width: 1px; stroke: #000; }
+.node-attribute:hover path { fill: #f6f6f6; }
+
+.edgeLabel text {
+    font-family: 'Open Sans', --apple-system, "Helvetica Neue", Helvetica, Arial, sans-serf;
+    font-size: 10px;
+}
+
+.edgePath path {
+    stroke: #000;
+    stroke-width: 1px;
+}
+
+.node rect {
+    stroke: none;
+    fill: none;
+    stroke-width: 0;
+}

+ 207 - 0
src/view-render.js

@@ -0,0 +1,207 @@
+
+function NodeFormatter(context) {
+    this.items = [];
+    this.properties = [];
+    this.attributes = [];
+}
+
+NodeFormatter.prototype.addItem = function(content, className, title, handler) {
+    var item = {};
+    if (content) {
+        item['content'] = content;
+    }
+    if (className) {
+        item['class'] = className;
+    }
+    if (title) {
+        item['title'] = title;
+    }
+    if (handler) {
+        item['handler'] = handler;
+    }
+    this.items.push(item);
+};
+
+NodeFormatter.prototype.addProperty = function(name, value) {
+    this.properties.push({ 'name': name, 'value': value });
+}
+
+NodeFormatter.prototype.setPropertyHandler = function(handler) {
+    this.propertyHandler = handler;
+}
+
+NodeFormatter.prototype.addAttribute = function(name, value) {
+    this.attributes.push({ 'name': name, 'value': value });
+}
+
+NodeFormatter.prototype.setAttributeHandler = function(handler) {
+    this.attributeHandler = handler;
+}
+
+NodeFormatter.prototype.format = function(context) {
+    var self = this;
+    var root = context.append('g');
+    var hasProperties = self.properties && self.properties.length > 0;
+    var hasAttributes = self.attributes && self.attributes.length > 0;
+    var x = 0;
+    var y = 0;
+    var maxHeight = 0;
+    var maxWidth = 0;
+    self.items.forEach(function (header, index) {
+        var yPadding = 4;
+        var xPadding = 7;
+        var itemGroup = root.append('g').classed('node-header', true).attr('transform', 'translate(' + x + ',' + y + ')');
+        var rect = itemGroup.append('path');
+        var text = itemGroup.append('text');
+        var content = header['content'];
+        var className = header['class']; 
+        var handler = header['handler'];
+        var title = header['title']
+        if (className) {
+            itemGroup.classed(className, true);
+        }
+        if (handler) {
+            itemGroup.on('click', handler);
+        }
+        if (title) {
+            itemGroup.append('title').text(title);
+        }
+        if (content) {
+            text.text(content);
+        }
+        var boundingBox = text.node().getBBox();
+        var width = boundingBox.width + xPadding + xPadding;
+        var height = boundingBox.height + yPadding + yPadding;
+        text.attr('x', xPadding);
+        text.attr('y', yPadding - boundingBox.y);
+        var r1 = index == 0;
+        var r2 = index == self.items.length - 1;
+        var r3 = !hasAttributes && !hasProperties && r2;
+        var r4 = !hasAttributes && !hasProperties && r1;
+        rect.attr('d', self.roundedRect(0, 0, width, height, r1, r2, r3, r4));
+        x += width;
+        if (height > maxHeight) {
+            maxHeight = height;
+        }
+        if (x > maxWidth) { 
+            maxWidth = x;
+        }
+    });
+
+    var itemWidth = maxWidth;
+    var itemHeight = maxHeight;
+
+    x = 0;
+    y += maxHeight;
+
+    var propertiesHeight = 0;
+    var propertiesPath = null;
+    if (hasProperties) {
+        var propertiesGroup = root.append('g').classed('node-property', true);
+        if (self.propertyHandler) {
+            propertiesGroup.on('click', self.propertyHandler);
+        }
+        propertiesPath = propertiesGroup.append('path');
+        propertiesGroup.attr('transform', 'translate(' + x + ',' + y + ')');
+        propertiesHeight += 4;
+        self.properties.forEach(function (property) {
+            var yPadding = 1;
+            var xPadding = 4;
+            var text = propertiesGroup.append('text').attr('xml:space', 'preserve');
+            var text_name = text.append('tspan').style('font-weight', 'bold').text(property.name);
+            var text_value = text.append('tspan').text(': ' + property.value)
+            var size = text.node().getBBox();
+            var width = xPadding + size.width + xPadding;
+            if (maxWidth < width) {
+                maxWidth = width;
+            }
+            text.attr('x', x + xPadding);
+            text.attr('y', propertiesHeight + yPadding - size.y);
+            propertiesHeight += yPadding + size.height + yPadding;
+        });
+        propertiesHeight += 4;
+    }
+
+    y += propertiesHeight;
+
+    var attributesHeight = 0;
+    var attributesPath = null;
+    if (hasAttributes)
+    {
+        var attributeGroup = root.append('g').classed('node-attribute', true);
+        if (self.attributeHandler) {
+            attributeGroup.on('click', self.attributeHandler);
+        }
+        attributesPath = attributeGroup.append('path');
+        attributeGroup.attr('transform', 'translate(' + x + ',' + y + ')');
+        attributesHeight += 4;
+        self.attributes.forEach(function (attribute) {
+            var yPadding = 1;
+            var xPadding = 4;
+            var text = attributeGroup.append('text').attr('xml:space', 'preserve');
+            var text_name = text.append('tspan').style('font-weight', 'bold').text(attribute.name);
+            var text_value = text.append('tspan').text(' = ' + attribute.value)
+            var size = text.node().getBBox();
+            var width = xPadding + size.width + xPadding;
+            if (maxWidth < width) {
+                maxWidth = width;
+            }
+            text.attr('x', x + xPadding);
+            text.attr('y', attributesHeight + yPadding - size.y);
+            attributesHeight += yPadding + size.height + yPadding;
+        });
+        attributesHeight += 4;
+    }
+
+    if (maxHeight < y) {
+        maxHeight = y;
+    }
+
+    if (maxWidth > itemWidth) {
+        var d = (maxWidth - itemWidth) / self.items.length;
+        self.items.forEach(function (header, index) {
+            var itemGroup = dagreD3.d3.select(root.node().children[index]);
+            var t = dagreD3.d3.transform(itemGroup.attr('transform')).translate;
+            itemGroup.attr('transform', 'translate(' + (t[0] + index * d) + ',' + t[1] + ')');
+            var path = dagreD3.d3.select(itemGroup.node().children[0]);
+            var r1 = index == 0;
+            var r2 = index == self.items.length - 1;
+            var r3 = !hasAttributes && !hasProperties && r2;
+            var r4 = !hasAttributes && !hasProperties && r1;
+            var box = path.node().getBBox();
+            path.attr('d', self.roundedRect(0, 0, box.width + d, box.height, r1, r2, r3, r4));
+            var text = dagreD3.d3.select(itemGroup.node().children[1]);
+            var t2 = dagreD3.d3.transform(text.attr('transform')).translate;
+            text.attr('transform', 'translate(' + (t2[0] + 0.5 * d) + ',' + t2[1] + ')');
+        });
+    }
+
+    if (hasProperties && propertiesPath) {
+        propertiesPath.attr('d', self.roundedRect(0, 0, maxWidth, propertiesHeight, false, false, !hasAttributes, !hasAttributes));
+    }
+
+    if (hasAttributes && attributesPath) {
+        attributesPath.attr('d', self.roundedRect(0, 0, maxWidth, attributesHeight, false, false, true, true));
+    }
+
+    context.html("");
+    return root;
+};
+
+NodeFormatter.prototype.roundedRect = function(x, y, width, height, r1, r2, r3, r4) {
+    var radius = 5;
+    r1 = r1 ? radius : 0;
+    r2 = r2 ? radius : 0;
+    r3 = r3 ? radius : 0;
+    r4 = r4 ? radius : 0;
+    return "M" + (x + r1) + "," + y
+       + "h" + (width - r1 - r2)
+       + "a" + r2 + "," + r2 + " 0 0 1 " + r2 + "," + r2
+       + "v" + (height - r2 - r3)
+       + "a" + r3 + "," + r3 + " 0 0 1 " + -r3 + "," + r3
+       + "h" + (r3 + r4 - width)
+       + "a" + r4 + "," + r4 + " 0 0 1 " + -r4 + "," + -r4
+       + 'v' + (-height + r4 + r1)
+       + "a" + r1 + "," + r1 + " 0 0 1 " + r1 + "," + -r1
+       + "z";
+};

+ 1 - 112
src/view.css

@@ -37,7 +37,7 @@ button {
     border: 1px solid #aaa;
 }
 
- button:hover {
+button:hover {
     background-color: #aaa;
     color: #e6e6e6;
     cursor: hand;
@@ -48,117 +48,6 @@ button:focus
     outline: 0;
 }
 
-text {
-    font-family: 'Open Sans', --apple-system, "Helvetica Neue", Helvetica, Arial, sans-serf;
-    font-size: 10px;
-}
-
-.edgePath path {
-    stroke: #000;
-    stroke-width: 1px;
-}
-
-.node rect {
-    stroke: none;
-    fill: none;
-    stroke-width: 0;
-}
-
-.input rect {
-    stroke: none;
-    fill: none;
-    stroke-width: 1px;
-}
-
-.input table {
-    font-family: 'Open Sans', --apple-system, "Helvetica Neue", Helvetica, Arial, sans-serf;
-    font-size: 10px;
-    background-color: #ddd;
-    border-spacing: 0;
-    border-collapse: separate;
-    border-radius: 10px;
-    border: 1px solid #333;
-    overflow: hidden;
-}
-
-.input table th {
-    padding: 5px;
-}
-
-.output rect {
-    stroke: none;
-    fill: none;
-    stroke-width: 0;
-}
-
-.output table {
-    font-family: 'Open Sans', --apple-system, "Helvetica Neue", Helvetica, Arial, sans-serf;
-    font-size: 10px;
-    background-color: #ddd;
-    border-spacing: 0;
-    border-collapse: separate;
-    border-radius: 10px;
-    border: 1px solid #333;
-    overflow: hidden;
-}
-
-.output table th {
-    padding: 5px;
-}
-
-.operator rect {
-    stroke: none;
-    fill: none;
-    stroke-width: 1px;
-}
-
-.operator table {
-    font-family: 'Open Sans', --apple-system, "Helvetica Neue", Helvetica, Arial, sans-serf;
-    font-size: 10px;
-    background-color: #fff;
-    border-spacing: 0;
-    border-collapse: separate;
-    border: 1px solid #333;
-    border-radius: 5px;
-    overflow: hidden;
-}
-
-.operator table th:first-child {
-    background-color: #333;
-    color: #fff;
-    border-left: 0;
-}
-
-.operator table th:first-child:hover {
-    background-color: #fff;
-    color: #333;
-}
-
-.operator table th {
-    font-size: 10px;
-    font-weight: bold;
-    background-color: #ddd;
-    padding: 5px;
-    border-left: 1px solid #333;
-}
-
-.operator table th:hover {
-    background-color: #fff;
-    color: #333;
-}
-
-.operator table td {
-    border-top: 1px solid #333;
-    font-size: 6px;
-    font-weight: normal;
-    padding: 3px 5px 3px 5px;
-}
-
-.operator table td:hover {
-    background-color: #e6e6e6;
-    color: #333;
-}
-
 .sidebar {
     font-family: 'Open Sans', --apple-system, "Helvetica Neue", Helvetica, Arial, sans-serf;
     font-size: 12px;

+ 36 - 74
src/view.js

@@ -69,6 +69,13 @@ function openBuffer(err, buffer) {
 }
 
 function renderModel(model) {
+
+    var svgElement = document.getElementById('graph');
+    while (svgElement.lastChild) {
+        svgElement.removeChild(svgElement.lastChild);
+    }
+    var svg = dagreD3.d3.select(svgElement);
+
     var g = new dagreD3.graphlib.Graph();
     g.setGraph({});
     g.setDefaultEdgeLabel(() => { return {}; });
@@ -87,13 +94,10 @@ function renderModel(model) {
 
         var operator = node.opType;
 
-        var element = document.createElement('table');
-        var content = dagreD3.d3.select(element);
-        var header = content.append('tr');
-        header.append('th').text(operator).style('cursor', 'pointer').on('click', function() {
-            showDocumentation(operator);
-        });
-    
+        var formatter = new NodeFormatter();
+
+        formatter.addItem(operator, 'node-header-operator', null, function() { showDocumentation(operator) });
+
         node.input.forEach(function (edge, index)
         {
             var name = modelService.getOperatorService().getInputName(operator, index);
@@ -104,15 +108,11 @@ function renderModel(model) {
             var initializer = initializerMap[edge];
             if (initializer) {
                 var type = formatTensorType(initializer);
-                var th = header.append('th');
-                th.style('cursor', 'pointer').on('click', function () {
-                    showInitializer(initializer);
-                });
-                th.append('span').attr('title', type).text(name);
+                formatter.addItem(name, 'node-header-input', type, function() { showInitializer(initializer); });
             }
             else {
                 // TODO is there a way to infer the type of the input?
-                header.append('th').style('background-color', '#f8f8f8').append('span').text(name);
+                formatter.addItem(name, null, null, null);
 
                 var tuple = edgeMap[edge];
                 if (!tuple) {
@@ -140,61 +140,33 @@ function renderModel(model) {
         });
 
         if (node.name || node.docString) {
-
-            var tr = content.append('tr');
-            var td = tr.append('td').attr('colspan', '100%');
-            tr.style('cursor', 'pointer').on('click', function() {
-                showNodeDetails(node);
-            });
-
-            var html = []; 
-            
             if (node.name) {
-                html.push('<span style="white-space: nowrap;"><b>name</b>: ' + node.name + "</b><br></span>");
+                formatter.addProperty('name', node.name);
             }
-            
             if (node.docString) {
-                var snippet = '<span style="white-space: nowrap;"'
                 var doc = node.docString;
                 if (doc.length > 50) {
-                    snippet += ' title="' + doc + '"';
                     doc = doc.substring(0, 25) + '...';
                 }
-                snippet += '><b>doc</b>: ' + doc + '</span><br>';
-                html.push(snippet);
+                formatter.addProperty('doc', doc);
             }
-
-            td.html(html.join(''));            
+            formatter.setPropertyHandler(function() { showNodeProperties(node) });
         }
 
         if (node.attribute && node.attribute.length > 0) {
-
-            var tr = content.append('tr');
-            var td = tr.append('td').attr('colspan', '100%');
-            tr.style('cursor', 'pointer').on('click', function() {
-                showAttributes(node.attribute);
+            node.attribute.forEach(function (attribute) {
+                var attributeValue = formatAttributeValue(attribute);
+                if (attributeValue.length > 25)
+                {
+                    attributeValue = attributeValue.substring(0, 25) + '...';
+                }
+                formatter.addAttribute(attribute.name, attributeValue);
             });
 
-            var html = []; 
-
-            if (node.attribute && node.attribute.length > 0) {
-                node.attribute.forEach(function (attribute) {
-                    var snippet = '<span style="white-space: nowrap;"'
-                    var attributeValue = formatAttributeValue(attribute);
-                    if (attributeValue.length > 25)
-                    {
-                        snippet += ' title="' + attributeValue + '"';
-                        attributeValue = attributeValue.substring(0, 25) + '...';
-                    }
-                    snippet += '><b>' + attribute.name + ' </b> = ' + attributeValue + '</span><br>';
-                    html.push(snippet);
-                });
-            }
-
-            td.html(html.join(''));
+            formatter.setAttributeHandler(function() { showNodeAttributes(node.attribute); });
         }
 
-        g.setNode(nodeId++, { label: element, class: 'operator', padding: 0 } );
+        g.setNode(nodeId++, { label: formatter.format(svg).node(), labelType: 'svg', padding: 0 });
     });
 
     graph.input.forEach(function (valueInfo) {
@@ -211,12 +183,9 @@ function renderModel(model) {
     
             var type = formatType(valueInfo.type);
 
-            var element = document.createElement('table');
-            var content = dagreD3.d3.select(element);
-            var header = content.append('tr');
-            header.append('th').append('span').attr('title', type).html(valueInfo.name);
-            
-            g.setNode(nodeId++, { label: element, class: 'input', padding: 0 } ); 
+            var formatter = new NodeFormatter();
+            formatter.addItem(valueInfo.name, null, type, null);
+            g.setNode(nodeId++, { label: formatter.format(svg).node(), labelType: 'svg', padding: 0 } ); 
         }
     });
 
@@ -231,11 +200,10 @@ function renderModel(model) {
             node: nodeId,
             // name: valueInfo.name
         });
-        var element = document.createElement('table');
-        var content = dagreD3.d3.select(element);
-        var header = content.append('tr');
-        header.append('th').html(valueInfo.name);
-        g.setNode(nodeId++, { label: element, class: 'output', padding: 0 } );        
+
+        var formatter = new NodeFormatter();
+        formatter.addItem(valueInfo.name, null, null, null);
+        g.setNode(nodeId++, { label: formatter.format(svg).node(), labelType: 'svg', padding: 0 } ); 
     });
 
     Object.keys(edgeMap).forEach(function (edge) {
@@ -265,12 +233,6 @@ function renderModel(model) {
         }
     });
 
-    var svgElement = document.getElementById('graph');
-    while (svgElement.lastChild) {
-        svgElement.removeChild(svgElement.lastChild);
-    }
-
-    var svg = dagreD3.d3.select(svgElement);
     var inner = svg.append('g');
 
     // Set up zoom support
@@ -414,7 +376,7 @@ function showInitializer(initializer) {
     openSidebar(data, 'Initializer');
 }
 
-function showNodeDetails(node) {
+function showNodeProperties(node) {
     if (node.name || node.docString) {
         
         var view = { 'attributes': [] };        
@@ -427,11 +389,11 @@ function showNodeDetails(node) {
 
         var template = Handlebars.compile(attributesTemplate, 'utf-8');
         var data = template(view);
-        openSidebar(data, 'Node Details');
+        openSidebar(data, 'Node Properties');
     }
 }
 
-function showAttributes(attributes) {
+function showNodeAttributes(attributes) {
     if (attributes && attributes.length > 0) {
 
         var view = { 'attributes': [] };        
@@ -451,7 +413,7 @@ function showAttributes(attributes) {
 
         var template = Handlebars.compile(attributesTemplate, 'utf-8');
         var data = template(view);
-        openSidebar(data, 'Attributes');
+        openSidebar(data, 'Node Attributes');
     }
 }