Selaa lähdekoodia

Target selector (#1441)

Lutz Roeder 11 kuukautta sitten
vanhempi
sitoutus
1b2ced7119
6 muutettua tiedostoa jossa 333 lisäystä ja 216 poistoa
  1. 5 3
      source/coreml.js
  2. 1 1
      source/electron.mjs
  3. 65 26
      source/index.html
  4. 9 0
      source/onnx.js
  5. 12 2
      source/tf.js
  6. 241 184
      source/view.js

+ 5 - 3
source/coreml.js

@@ -213,6 +213,7 @@ coreml.Model = class {
         this.format = context.format;
         this.metadata = Array.from(context.metadata);
         this.graphs = context.graphs.map((context) => new coreml.Graph(context));
+        this.functions = context.functions.map((context) => new coreml.Graph(context));
         if (context.version) {
             this.version = context.version;
         }
@@ -532,16 +533,17 @@ coreml.Context = class {
         this.format = format;
         this.metadata = [];
         this.graphs = [];
+        this.functions = [];
         const description = model.description;
         for (const func of description.functions) {
             const graph = new coreml.Context.Graph(metadata, func.name, 'function', model, func, weights, values);
-            this.graphs.push(graph);
+            this.functions.push(graph);
         }
         if (description && description.defaultFunctionName) {
             const graph = this.graphs.find((graph) => graph.name === description.defaultFunctionName);
             if (graph) {
-                this.graphs.splice(this.graphs.indexOf(graph), 1);
-                this.graphs.unshift(graph);
+                this.functions.splice(this.graphs.indexOf(graph), 1);
+                this.functions.unshift(graph);
             }
         }
         if (model && !model.mlProgram || (model.mlProgram.functions && model.mlProgram.functions.main)) {

+ 1 - 1
source/electron.mjs

@@ -196,7 +196,7 @@ host.ElectronHost = class {
             this._view.resetZoom();
         });
         electron.ipcRenderer.on('show-properties', () => {
-            this._element('sidebar-button').click();
+            this._element('sidebar-target-button').click();
         });
         electron.ipcRenderer.on('find', () => {
             this._view.find();

+ 65 - 26
source/index.html

@@ -30,11 +30,27 @@ button { font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI"
 .default .toolbar { display: table; }
 .toolbar { position: absolute; bottom: 10px; left: 10px; padding: 0; margin: 0; user-select: none; -webkit-user-select: none; -moz-user-select: none; }
 .toolbar button:focus { outline: 0; }
-.toolbar-button { float: left; background: None; border-radius: 6px; border: 0; margin: 0; margin-right: 1px; padding: 0; fill: None; stroke: #777; cursor: pointer; width: 24px; height: 24px; user-select: none; }
+.toolbar-button { float: left; background: None; border-radius: 6px; border: 0; margin: 0; margin-right: 1px; padding: 0; fill: None; stroke: #777; cursor: pointer; width: 32px; height: 32px; user-select: none; }
+.toolbar-select { margin-top: 4px; margin-bottom: 4px; padding-top: 5px; padding-bottom: 5px; }
 .toolbar-path { float: left }
-.toolbar-path-back-button { float: left; background: #777; border-top-left-radius: 6px; border-bottom-left-radius: 6px; border: 0px solid; border-color: #777; margin: 2px 0px 2px 8px; padding: 0 8px 0 8px; cursor: pointer; height: 20px; color: #ffffff; font-size: 11px; line-height: 0; transition: 0.1s; }
+.toolbar-select { 
+    appearance: none; -webkit-appearance: none; -moz-appearance: none;
+    font-size: 12px;
+    line-height: 12px;
+    margin: 4px 4px 4px 4px;
+    border: 1px solid;
+    padding: 5px 10px 5px 10px;
+    border-radius: 6px;
+    width: 170px;
+    background: #777;
+    border-color: #777;
+    color: #fff;
+}
+.toolbar-select:hover { background: #000000; border-color: #000000; }
+.toolbar-select:focus { outline: 0; }
+.toolbar-path-back-button { float: left; background: #777; border-top-left-radius: 6px; border-bottom-left-radius: 6px; border: 1px solid; border-color: #777; margin: 4px 0px 4px 4px; padding: 5px 8px 5px 8px; cursor: pointer; color: #ffffff; font-size: 12px; line-height: 12px; transition: 0.1s; }
 .toolbar-path-back-button:hover { background: #000000; border-color: #000000; }
-.toolbar-path-name-button { float: left; background: #777; border: 0px solid; border-color: #777; color: #ffffff; border-left: 1px; border-left-color: #ffffff; margin: 2px 0 2px 1px; padding: 0 8px 0 8px; cursor: pointer; width: auto; height: 20px; font-size: 11px; line-height: 0; transition: 0.1s; }
+.toolbar-path-name-button { float: left; background: #777; border: 0px; border-color: #777; color: #ffffff; border-left: 1px; border-left-color: #ffffff; margin: 4px 0 4px 1px; padding: 6px 8px 6px 8px; cursor: pointer; width: auto; font-size: 12px; line-height: 12px; transition: 0.1s; }
 .toolbar-path-name-button:hover { background: #000000; border-color: #000000; }
 .toolbar-path-name-button:last-child { border-top-right-radius: 6px; border-bottom-right-radius: 6px; }
 .toolbar-icon .border { stroke: #fff; }
@@ -153,6 +169,8 @@ button { font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI"
 .toolbar-path-back-button:hover { background: #dfdfdf; border-color: #dfdfdf; }
 .toolbar-path-name-button { background: #aaaaaa ; border-color: #aaaaaa; color: #404040; }
 .toolbar-path-name-button:hover { background: #dfdfdf; border-color: #dfdfdf; }
+.toolbar-select { background: #aaaaaa; border-color: #aaaaaa; color: #404040; }
+.toolbar-select:hover { background: #dfdfdf; border-color: #dfdfdf; color: #404040; }
 .titlebar { color: #949494; }
 .welcome body { background-color: #1e1e1e; }
 .default body { background-color: #404040; }
@@ -182,11 +200,6 @@ button { font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI"
 .about .logo-message { top: 175px; font-size: 12px; }
 .about .logo-github { top: 370px; }
 }
-@media only screen and (max-device-width: 1024px) {
-.toolbar-button { width: 32px; height: 32px; }
-.toolbar-path-back-button { margin-top: 6px; margin-bottom: 6px; }
-.toolbar-path-name-button { margin-top: 6px; margin-bottom: 6px; }
-}
 .sidebar { display: flex; flex-direction: column; font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "Ubuntu", "Droid Sans", sans-serif; font-size: 12px; height: 100%; right: -100%; position: fixed; transition: 0.1s; top: 0; background-color: #ececec; color: #242424; overflow: hidden; border-left: 1px solid rgba(255, 255, 255, 0.5); opacity: 0; }
 .sidebar-title { font-weight: bold; font-size: 12px; letter-spacing: 0.5px; text-transform: uppercase; height: 20px; margin: 0; padding: 20px; user-select: none; -webkit-user-select: none; -moz-user-select: none; }
 .sidebar-closebutton { padding: 8px 8px 8px 32px; text-decoration: none; font-size: 25px; color: #777777; opacity: 1.0; display: block; transition: 0.2s; position: absolute; top: 0; right: 15px; margin-left: 50px; user-select: none; -webkit-user-select: none; -moz-user-select: none; }
@@ -323,24 +336,6 @@ button { font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI"
     </svg>
 </div>
 <div id="toolbar" class="toolbar">
-    <button id="sidebar-button" class="toolbar-button" title="Model Properties">
-        <svg class="toolbar-icon" viewbox="0 0 100 100">
-            <rect class="border" x="12" y="12" width="76" height="76" rx="16" ry="16" stroke-width="8"></rect>
-            <line class="border" x1="28" y1="37" x2="32" y2="37" stroke-width="8" stroke-linecap="round" stroke="#fff"></line>
-            <line class="border" x1="28" y1="50" x2="32" y2="50" stroke-width="8" stroke-linecap="round" stroke="#fff"></line>
-            <line class="border" x1="28" y1="63" x2="32" y2="63" stroke-width="8" stroke-linecap="round" stroke="#fff"></line>
-            <line class="border" x1="40" y1="37" x2="70" y2="37" stroke-width="8" stroke-linecap="round" stroke="#fff"></line>
-            <line class="border" x1="40" y1="50" x2="70" y2="50" stroke-width="8" stroke-linecap="round" stroke="#fff"></line>
-            <line class="border" x1="40" y1="63" x2="70" y2="63" stroke-width="8" stroke-linecap="round" stroke="#fff"></line>
-            <rect class="stroke" x="12" y="12" width="76" height="76" rx="16" ry="16" stroke-width="4"></rect>
-            <line class="stroke" x1="28" y1="37" x2="32" y2="37" stroke-width="4" stroke-linecap="round"></line>
-            <line class="stroke" x1="28" y1="50" x2="32" y2="50" stroke-width="4" stroke-linecap="round"></line>
-            <line class="stroke" x1="28" y1="63" x2="32" y2="63" stroke-width="4" stroke-linecap="round"></line>
-            <line class="stroke" x1="40" y1="37" x2="70" y2="37" stroke-width="4" stroke-linecap="round"></line>
-            <line class="stroke" x1="40" y1="50" x2="70" y2="50" stroke-width="4" stroke-linecap="round"></line>
-            <line class="stroke" x1="40" y1="63" x2="70" y2="63" stroke-width="4" stroke-linecap="round"></line>
-        </svg>
-    </button>
     <button id="zoom-in-button" class="toolbar-button" title="Zoom In">
         <svg class="toolbar-icon" viewbox="0 0 100 100">
             <circle class="border" cx="50" cy="50" r="35" stroke-width="8" stroke="#fff"></circle>
@@ -363,6 +358,50 @@ button { font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI"
             <line class="stroke" x1="78" y1="78" x2="82" y2="82" stroke-width="8" stroke-linecap="square"></line>
         </svg>
     </button>
+    <button id="sidebar-document-button" class="toolbar-button" title="Model Properties">
+        <svg class="toolbar-icon" viewbox="0 0 100 100">
+            <rect class="border" x="12" y="12" width="76" height="76" rx="16" ry="16" stroke-width="8"></rect>
+            <line class="border" x1="28" y1="37" x2="32" y2="37" stroke-width="8" stroke-linecap="round" stroke="#fff"></line>
+            <line class="border" x1="28" y1="50" x2="32" y2="50" stroke-width="8" stroke-linecap="round" stroke="#fff"></line>
+            <line class="border" x1="28" y1="63" x2="32" y2="63" stroke-width="8" stroke-linecap="round" stroke="#fff"></line>
+            <line class="border" x1="40" y1="37" x2="70" y2="37" stroke-width="8" stroke-linecap="round" stroke="#fff"></line>
+            <line class="border" x1="40" y1="50" x2="70" y2="50" stroke-width="8" stroke-linecap="round" stroke="#fff"></line>
+            <line class="border" x1="40" y1="63" x2="70" y2="63" stroke-width="8" stroke-linecap="round" stroke="#fff"></line>
+            <rect class="stroke" x="12" y="12" width="76" height="76" rx="16" ry="16" stroke-width="4"></rect>
+            <line class="stroke" x1="28" y1="37" x2="32" y2="37" stroke-width="4" stroke-linecap="round"></line>
+            <line class="stroke" x1="28" y1="50" x2="32" y2="50" stroke-width="4" stroke-linecap="round"></line>
+            <line class="stroke" x1="28" y1="63" x2="32" y2="63" stroke-width="4" stroke-linecap="round"></line>
+            <line class="stroke" x1="40" y1="37" x2="70" y2="37" stroke-width="4" stroke-linecap="round"></line>
+            <line class="stroke" x1="40" y1="50" x2="70" y2="50" stroke-width="4" stroke-linecap="round"></line>
+            <line class="stroke" x1="40" y1="63" x2="70" y2="63" stroke-width="4" stroke-linecap="round"></line>
+        </svg>
+    </button>
+    <button id="sidebar-target-button" class="toolbar-button" title="Target Properties">
+        <svg class="toolbar-icon" viewBox="0 0 100 100">
+            <rect class="border" x="12" y="12" width="76" height="76" rx="16" ry="16" stroke-width="8"></rect>
+            <circle class="border" cx="35" cy="50" r="8" stroke-width="8"></circle>
+            <circle class="border" cx="65" cy="35" r="8" stroke-width="8"></circle>
+            <circle class="border" cx="65" cy="65" r="8" stroke-width="8"></circle>
+            <line class="border" x1="41.6" y1="46.8" x2="58.4" y2="38.2" stroke-width="8" />
+            <line class="border" x1="41.6" y1="53.2" x2="58.4" y2="61.8" stroke-width="8" />
+            <rect class="stroke" x="12" y="12" width="76" height="76" rx="16" ry="16" stroke-width="4"></rect>
+            <circle class="stroke" cx="35" cy="50" r="8" stroke-width="4"></circle>
+            <circle class="stroke" cx="65" cy="35" r="8" stroke-width="4"></circle>
+            <circle class="stroke" cx="65" cy="65" r="8" stroke-width="4"></circle>
+            <line class="stroke" x1="41.6" y1="46.8" x2="58.4" y2="38.2" stroke-width="4" />
+            <line class="stroke" x1="41.6" y1="53.2" x2="58.4" y2="61.8" stroke-width="4" />
+        </svg>
+    </button>
+    <div id="toolbar-navigator" class="toolbar-path">
+        <select id="toolbar-target-selector" class="toolbar-select">
+            <option value="Graphs" disabled="true">&#x2014; Graphs &#x2014;&#x2014;</option>
+            <option value="add">add</option>
+            <option value="subtract">subtract</option>
+            <option value="multiply">multiply</option>
+            <option value="Functions" disabled="true">&#x2014; Functions &#x2014;&#x2014;</option>
+            <option value="line">foo_bar.foo</option>
+        </select>
+    </div>
     <div id="toolbar-path" class="toolbar-path">
         <button id="toolbar-path-back-button" class="toolbar-path-back-button" title="Back">
             &#x276E;

+ 9 - 0
source/onnx.js

@@ -120,6 +120,7 @@ onnx.Model = class {
         if (graph) {
             this._graphs.push(graph);
         }
+        this._functions = context.functions;
     }
 
     get format() {
@@ -157,6 +158,10 @@ onnx.Model = class {
     get graphs() {
         return this._graphs;
     }
+
+    get functions() {
+        return this._functions;
+    }
 };
 
 onnx.Graph = class {
@@ -933,6 +938,10 @@ onnx.Context.Model = class {
         return this._graphs.get(value);
     }
 
+    get functions() {
+        return Array.from(this._functions.values());
+    }
+
     location(name) {
         if (this._locations.has(name)) {
             return this._locations.get(name);

+ 12 - 2
source/tf.js

@@ -729,6 +729,7 @@ tf.Graph = class {
         this.nodes = [];
         this.inputs = [];
         this.outputs = [];
+        this.functions = [];
         this.signatures = [];
         this.version = null;
         this.metadata = [];
@@ -747,6 +748,7 @@ tf.Graph = class {
             }
             const output_arg_map = new Map();
             metadata = new tf.GraphMetadata(metadata, graph.library);
+            this.functions = metadata.functions;
             const context = new tf.Context();
             for (const [key, signature_def] of Object.entries(meta_graph.signature_def)) {
                 const inputs = [];
@@ -1633,8 +1635,7 @@ tf.GraphMetadata = class {
         this._functions = new Map();
         this._attributes = new Map();
         this._visibleCache = new Map();
-
-        if (library && Array.isArray(library.function)) {
+        if (library && Array.isArray(library.function) && library.function.length > 0) {
             for (const func of library.function) {
                 const name = func.signature.name;
                 if (this._functions.has(func.name)) {
@@ -1708,6 +1709,15 @@ tf.GraphMetadata = class {
         }
         return !this._visibleCache.get(type).has(name);
     }
+
+    get functions() {
+        for (const [name, func] of this._functions) {
+            if (func instanceof tf.Function === false) {
+                this._functions.set(name, new tf.Function(this, func.signature.name, func));
+            }
+        }
+        return Array.from(this._functions.values());
+    }
 };
 
 tf.Context = class {

+ 241 - 184
source/view.js

@@ -37,8 +37,11 @@ view.View = class {
             for (const [name, value] of Object.entries(options)) {
                 this._options[name] = value;
             }
-            this._element('sidebar-button').addEventListener('click', () => {
-                this.showModelProperties();
+            this._element('sidebar-document-button').addEventListener('click', () => {
+                this.showDocumentProperties();
+            });
+            this._element('sidebar-target-button').addEventListener('click', () => {
+                this.showTargetProperties();
             });
             this._element('zoom-in-button').addEventListener('click', () => {
                 this.zoomIn();
@@ -91,7 +94,7 @@ view.View = class {
                         label: '&Export...',
                         accelerator: 'CmdOrCtrl+Shift+E',
                         execute: async () => await this._host.execute('export'),
-                        enabled: () => this.activeGraph
+                        enabled: () => this.activeTarget
                     });
                     file.add({
                         label: platform === 'darwin' ? '&Close Window' : '&Close',
@@ -108,13 +111,13 @@ view.View = class {
                         label: 'Export as &PNG',
                         accelerator: 'CmdOrCtrl+Shift+E',
                         execute: async () => await this.export(`${this._host.document.title}.png`),
-                        enabled: () => this.activeGraph
+                        enabled: () => this.activeTarget
                     });
                     file.add({
                         label: 'Export as &SVG',
                         accelerator: 'CmdOrCtrl+Alt+E',
                         execute: async () => await this.export(`${this._host.document.title}.svg`),
-                        enabled: () => this.activeGraph
+                        enabled: () => this.activeTarget
                     });
                 }
                 const edit = this._menu.group('&Edit');
@@ -122,38 +125,38 @@ view.View = class {
                     label: '&Find...',
                     accelerator: 'CmdOrCtrl+F',
                     execute: () => this.find(),
-                    enabled: () => this.activeGraph
+                    enabled: () => this.activeTarget
                 });
                 const view = this._menu.group('&View');
                 view.add({
                     label: () => this.options.attributes ? 'Hide &Attributes' : 'Show &Attributes',
                     accelerator: 'CmdOrCtrl+D',
                     execute: () => this.toggle('attributes'),
-                    enabled: () => this.activeGraph
+                    enabled: () => this.activeTarget
                 });
                 view.add({
                     label: () => this.options.weights ? 'Hide &Weights' : 'Show &Weights',
                     accelerator: 'CmdOrCtrl+I',
                     execute: () => this.toggle('weights'),
-                    enabled: () => this.activeGraph
+                    enabled: () => this.activeTarget
                 });
                 view.add({
                     label: () => this.options.names ? 'Hide &Names' : 'Show &Names',
                     accelerator: 'CmdOrCtrl+U',
                     execute: () => this.toggle('names'),
-                    enabled: () => this.activeGraph
+                    enabled: () => this.activeTarget
                 });
                 view.add({
                     label: () => this.options.direction === 'vertical' ? 'Show &Horizontal' : 'Show &Vertical',
                     accelerator: 'CmdOrCtrl+K',
                     execute: () => this.toggle('direction'),
-                    enabled: () => this.activeGraph
+                    enabled: () => this.activeTarget
                 });
                 view.add({
                     label: () => this.options.mousewheel === 'scroll' ? '&Mouse Wheel: Zoom' : '&Mouse Wheel: Scroll',
                     accelerator: 'CmdOrCtrl+M',
                     execute: () => this.toggle('mousewheel'),
-                    enabled: () => this.activeGraph
+                    enabled: () => this.activeTarget
                 });
                 view.add({});
                 if (this._host.type === 'Electron') {
@@ -161,7 +164,7 @@ view.View = class {
                         label: '&Reload',
                         accelerator: platform === 'darwin' ? 'CmdOrCtrl+R' : 'F5',
                         execute: async () => await this._host.execute('reload'),
-                        enabled: () => this.activeGraph
+                        enabled: () => this.activeTarget
                     });
                     view.add({});
                 }
@@ -169,26 +172,26 @@ view.View = class {
                     label: 'Zoom &In',
                     accelerator: 'Shift+Up',
                     execute: () => this.zoomIn(),
-                    enabled: () => this.activeGraph
+                    enabled: () => this.activeTarget
                 });
                 view.add({
                     label: 'Zoom &Out',
                     accelerator: 'Shift+Down',
                     execute: () => this.zoomOut(),
-                    enabled: () => this.activeGraph
+                    enabled: () => this.activeTarget
                 });
                 view.add({
                     label: 'Actual &Size',
                     accelerator: 'Shift+Backspace',
                     execute: () => this.resetZoom(),
-                    enabled: () => this.activeGraph
+                    enabled: () => this.activeTarget
                 });
                 view.add({});
                 view.add({
                     label: '&Properties...',
                     accelerator: 'CmdOrCtrl+Enter',
-                    execute: () => this.showModelProperties(),
-                    enabled: () => this.activeGraph
+                    execute: () => this.showTargetProperties(),
+                    enabled: () => this.activeTarget
                 });
                 if (this._host.type === 'Electron' && !this._host.environment('packaged')) {
                     view.add({});
@@ -208,6 +211,9 @@ view.View = class {
                     execute: async () => await this._host.execute('about')
                 });
             }
+
+            this._select = new view.TargetSelector(this, this._element('toolbar-target-selector'));
+            this._select.on('change', (sender, target) => this._updateActive([target]));
             await this._host.start();
         } catch (error) {
             this.error(error, null, null);
@@ -220,7 +226,7 @@ view.View = class {
 
     show(page) {
         if (!page) {
-            page = (!this._model && !this.activeGraph) ? 'welcome' : 'default';
+            page = (!this._model && !this.activeTarget) ? 'welcome' : 'default';
         }
         this._host.event('screen_view', {
             screen_name: page,
@@ -257,7 +263,7 @@ view.View = class {
     find() {
         if (this._graph && this._sidebar.identifier !== 'find') {
             this._graph.select(null);
-            const sidebar = new view.FindSidebar(this, this._find, this.activeGraph, this.activeSignature);
+            const sidebar = new view.FindSidebar(this, this._find, this.activeTarget, this.activeSignature);
             sidebar.on('state-changed', (sender, state) => {
                 this._find = state;
             });
@@ -698,11 +704,10 @@ view.View = class {
             const stack = [];
             if (Array.isArray(model.graphs) && model.graphs.length > 0) {
                 const [graph] = model.graphs;
-                const entry = {
-                    graph,
-                    signature: Array.isArray(graph.signatures) && graph.signatures.length > 0 ? graph.signatures[0] : null
-                };
-                stack.push(entry);
+                const signature = Array.isArray(graph.signatures) && graph.signatures.length > 0 ? graph.signatures[0] : null;
+                stack.push({ target: graph, signature });
+            } else if (Array.isArray(model.functions) && model.functions.length > 0) {
+                stack.push({ target: model.functions[0], signature: null });
             }
             return await this._updateGraph(model, stack);
         } catch (error) {
@@ -726,9 +731,9 @@ view.View = class {
         }
     }
 
-    get activeGraph() {
+    get activeTarget() {
         if (Array.isArray(this._stack) && this._stack.length > 0) {
-            return this._stack[0].graph;
+            return this._stack[0].target;
         }
         return null;
     }
@@ -744,11 +749,11 @@ view.View = class {
         const update = async (model, stack) => {
             this._model = model;
             this._stack = stack;
-            const status = await this.renderGraph(this._model, this.activeGraph, this.activeSignature, this._options);
+            const status = await this.renderGraph(this._model, this.activeTarget, this.activeSignature, this._options);
             if (status !== '') {
                 this._model = null;
                 this._stack = [];
-                this._activeGraph = null;
+                this._activeTarget = null;
             }
             this.show(null);
             const path = this._element('toolbar-path');
@@ -770,7 +775,7 @@ view.View = class {
                         path.appendChild(element);
                     }
                     for (let i = count; i >= 0; i--) {
-                        const graph = this._stack[i].graph;
+                        const target = this._stack[i].target;
                         const element = this._host.document.createElement('button');
                         element.setAttribute('class', 'toolbar-path-name-button');
                         element.addEventListener('click', async () => {
@@ -778,14 +783,14 @@ view.View = class {
                                 this._stack = this._stack.slice(i);
                                 await this._updateGraph(this._model, this._stack);
                             } else {
-                                await this.showDefinition(graph);
+                                await this.showTargetProperties(target);
                             }
                         });
                         let name = '';
-                        if (graph && graph.identifier) {
-                            name = graph.identifier;
-                        } else if (graph && graph.name) {
-                            name = graph.name;
+                        if (target && target.identifier) {
+                            name = target.identifier;
+                        } else if (target && target.name) {
+                            name = target.name;
                         }
                         if (name.length > 24) {
                             element.setAttribute('title', name);
@@ -797,6 +802,16 @@ view.View = class {
                         path.appendChild(element);
                     }
                 }
+                this._select.update(model, stack);
+                const button = this._element('sidebar-target-button');
+                if (stack.length > 0) {
+                    const type = stack[stack.length - 1].type || 'graph';
+                    const name = type.charAt(0).toUpperCase() + type.slice(1);
+                    button.setAttribute('title', `${name} Properties`);
+                    button.style.display = 'block';
+                } else {
+                    button.style.display = 'none';
+                }
             }
         };
         const lastModel = this._model;
@@ -811,14 +826,14 @@ view.View = class {
     }
 
     async pushGraph(graph, context) {
-        if (graph && graph !== this.activeGraph && Array.isArray(graph.nodes)) {
+        if (graph && graph !== this.activeTarget && Array.isArray(graph.nodes)) {
             this._sidebar.close();
             if (context) {
                 this._stack[0].context = context;
                 this._stack[0].zoom = this._zoom;
             }
             const signature = Array.isArray(graph.signatures) && graph.signatures.length > 0 ? graph.signatures[0] : null;
-            const entry = { graph, signature };
+            const entry = { target: graph, signature };
             const stack = [entry].concat(this._stack);
             await this._updateGraph(this._model, stack);
         }
@@ -949,7 +964,7 @@ view.View = class {
     async export(file) {
         const lastIndex = file.lastIndexOf('.');
         const extension = lastIndex === -1 ? 'png' : file.substring(lastIndex + 1).toLowerCase();
-        if (this.activeGraph && (extension === 'png' || extension === 'svg')) {
+        if (this.activeTarget && (extension === 'png' || extension === 'svg')) {
             const canvas = this._element('canvas');
             const clone = canvas.cloneNode(true);
             this.applyStyleSheet(clone, 'grapher.css');
@@ -1023,43 +1038,61 @@ view.View = class {
         }
     }
 
-    showModelProperties() {
-        if (this._model) {
-            try {
-                const sidebar = new view.ModelSidebar(this, this.model, this.activeGraph, this.activeSignature);
-                sidebar.on('update-active-graph', (sender, graph) => {
-                    const entry = {
-                        graph,
-                        signature: Array.isArray(graph.signatures) && graph.signatures.length > 0 ? graph.signatures[0] : null
-                    };
-                    this._updateActive([entry]);
-                });
-                sidebar.on('update-active-graph-signature', (sender, signature) => {
-                    const stack = this._stack.map((entry) => {
-                        return { graph: entry.graph, signature: entry.signature };
-                    });
-                    stack[0].signature = signature;
-                    this._updateActive(stack);
-                });
-                sidebar.on('focus', (sender, value) => {
-                    this._graph.focus([value]);
-                });
-                sidebar.on('blur', (sender, value) => {
-                    this._graph.blur([value]);
-                });
-                sidebar.on('select', (sender, value) => {
-                    this.scrollTo(this._graph.activate(value));
-                });
-                sidebar.on('activate', (sender, value) => {
-                    this.scrollTo(this._graph.select([value]));
-                });
-                sidebar.on('deactivate', () => {
-                    this._graph.select(null);
-                });
-                this._sidebar.open(sidebar, 'Model Properties');
-            } catch (error) {
-                this.error(error, 'Error showing model properties.', null);
+    showDocumentProperties() {
+        if (!this._model) {
+            return;
+        }
+        try {
+            const sidebar = new view.ModelSidebar(this, this.model);
+            this._sidebar.open(sidebar, 'Model Properties');
+        } catch (error) {
+            this.error(error, 'Error showing model properties.', null);
+        }
+    }
+
+    showTargetProperties() {
+        const target = this.activeTarget;
+        if (!target) {
+            return;
+        }
+        try {
+            const sidebar = new view.TargetSidebar(this, target, this.activeSignature);
+            sidebar.on('show-definition', async (/* sender, e */) => {
+                await this.showDefinition(target);
+            });
+            sidebar.on('focus', (sender, value) => {
+                this._graph.focus([value]);
+            });
+            sidebar.on('blur', (sender, value) => {
+                this._graph.blur([value]);
+            });
+            sidebar.on('select', (sender, value) => {
+                this.scrollTo(this._graph.activate(value));
+            });
+            sidebar.on('activate', (sender, value) => {
+                this.scrollTo(this._graph.select([value]));
+            });
+            sidebar.on('deactivate', () => {
+                this._graph.select(null);
+            });
+            let title = null;
+            const type = target.type || 'graph';
+            switch (type) {
+                case 'graph':
+                    title = 'Graph Properties';
+                    break;
+                case 'function':
+                    title = 'Function Properties';
+                    break;
+                case 'weights':
+                    title = 'Weights Properties';
+                    break;
+                default:
+                    throw new view.Error(`Unsupported graph type '${type}'.`);
             }
+            this._sidebar.open(sidebar, title);
+        } catch (error) {
+            this.error(error, 'Error showing target properties.', null);
         }
     }
 
@@ -1157,7 +1190,7 @@ view.View = class {
                 sidebar.on('navigate', (sender, e) => {
                     this._host.openURL(e.link);
                 });
-                const title = type.type === 'function' ? 'Function' : 'Documentation';
+                const title = type.type === 'function' ? 'Function Documentation' : 'Documentation';
                 this._sidebar.push(sidebar, title);
             }
         }
@@ -2223,7 +2256,7 @@ view.Input = class extends grapher.Node {
         }
         const header = this.header();
         const title = header.add(null, ['graph-item-input'], name, types);
-        title.on('click', () => this.context.view.showModelProperties());
+        title.on('click', () => this.context.view.showTargetProperties());
         this.id = `input-${name ? `name-${name}` : `id-${(view.Input.counter++)}`}`;
     }
 
@@ -2240,7 +2273,7 @@ view.Input = class extends grapher.Node {
     }
 
     activate() {
-        this.context.view.showModelProperties();
+        this.context.view.showTargetProperties();
     }
 
     edge(to) {
@@ -2265,7 +2298,7 @@ view.Output = class extends grapher.Node {
         }
         const header = this.header();
         const title = header.add(null, ['graph-item-output'], name, types);
-        title.on('click', () => this.context.view.showModelProperties());
+        title.on('click', () => this.context.view.showTargetProperties());
     }
 
     get inputs() {
@@ -2277,7 +2310,7 @@ view.Output = class extends grapher.Node {
     }
 
     activate() {
-        this.context.view.showModelProperties();
+        this.context.view.showTargetProperties();
     }
 };
 
@@ -2619,6 +2652,72 @@ view.Expander = class extends view.Control {
     }
 };
 
+view.TargetSelector = class extends view.Control {
+
+    constructor(context, element) {
+        super(context);
+        this._element = element;
+        this._element.addEventListener('change', (e) => {
+            const target = this._targets[e.target.selectedIndex];
+            this.emit('change', target);
+        });
+        this._targets = [];
+    }
+
+    update(model, stack) {
+        while (this._element.firstChild) {
+            this._element.removeChild(this._element.firstChild);
+        }
+        this._targets = [];
+        const current = stack.length > 0 ? stack[stack.length - 1] : null;
+        const section = (title, targets) => {
+            if (targets.length > 0) {
+                const group = this.createElement('optgroup');
+                group.setAttribute('label', title);
+                this._element.appendChild(group);
+                for (let i = 0; i < targets.length; i++) {
+                    const target = targets[i];
+                    const option = this.createElement('option');
+                    option.innerText = target.name;
+                    group.appendChild(option);
+                    if (current && current.target === target.target && current.signature === target.signature) {
+                        option.setAttribute('selected', 'true');
+                        this._element.setAttribute('title', target.name);
+                    }
+                    this._targets.push(target);
+                }
+            }
+        };
+        const graphs = [];
+        const signatures = [];
+        const functions = [];
+        for (const graph of model.graphs) {
+            const name = graph.name || '(unnamed)';
+            graphs.push({ name, target: graph, signature: null });
+            if (Array.isArray(graph.functions)) {
+                for (const func of graph.functions) {
+                    functions.push({ name: `${name}.${func.name}`, target: func, signature: null });
+                }
+            }
+            if (Array.isArray(graph.signatures)) {
+                for (const signature of graph.signatures) {
+                    signatures.push({ name: `${name}.${signature.name}`, target: graph, signature });
+                }
+            }
+        }
+        if (Array.isArray(model.functions)) {
+            for (const func of model.functions) {
+                functions.push({ name: func.name, target: func, signature: null });
+            }
+        }
+        section('Graphs', graphs);
+        section('Signatures', signatures);
+        section('Functions', functions);
+        const visible = functions.length > 0 || signatures.length > 0 || graphs.length > 1;
+        this._element.style.display = visible ? 'inline' : 'none';
+    }
+};
+
 view.ObjectSidebar = class extends view.Control {
 
     constructor(context) {
@@ -2808,32 +2907,6 @@ view.NameValueView = class extends view.Control {
     }
 };
 
-view.SelectView = class extends view.Control {
-
-    constructor(context, entries, selected) {
-        super(context);
-        this._elements = [];
-        this._entries = Array.from(entries);
-        const selectElement = this.createElement('select', 'sidebar-item-selector');
-        selectElement.addEventListener('change', (e) => {
-            this.emit('change', this._entries[e.target.selectedIndex][1]);
-        });
-        this._elements.push(selectElement);
-        for (const [name, value] of this._entries) {
-            const element = this.createElement('option');
-            element.innerText = name;
-            if (value === selected) {
-                element.setAttribute('selected', 'selected');
-            }
-            selectElement.appendChild(element);
-        }
-    }
-
-    render() {
-        return this._elements;
-    }
-};
-
 view.TextView = class extends view.Control {
 
     constructor(context, value, style) {
@@ -3558,11 +3631,9 @@ view.TensorSidebar = class extends view.ObjectSidebar {
 
 view.ModelSidebar = class extends view.ObjectSidebar {
 
-    constructor(context, model, graph, signature) {
+    constructor(context, model) {
         super(context);
         this._model = model;
-        this._graph = graph;
-        this._signature = signature;
     }
 
     get identifier() {
@@ -3571,8 +3642,6 @@ view.ModelSidebar = class extends view.ObjectSidebar {
 
     render() {
         const model = this._model;
-        const graph = this._graph;
-        const signature = this._signature;
         if (model.format) {
             this.addProperty('format', model.format);
         }
@@ -3600,28 +3669,6 @@ view.ModelSidebar = class extends view.ObjectSidebar {
         if (model.source) {
             this.addProperty('source', model.source);
         }
-        const graphs = Array.isArray(model.graphs) ? model.graphs : [];
-        if (graphs.length === 1 && graphs[0].name) {
-            this.addProperty('graph', graphs[0].name);
-        } else if (graphs.length > 1) {
-            const entries = new Map();
-            for (const graph of model.graphs) {
-                entries.set(graph.name, graph);
-            }
-            const selector = new view.SelectView(this._view, entries, graph);
-            selector.on('change', (sender, data) => this.emit('update-active-graph', data));
-            this.addEntry('graph', selector);
-        }
-        if (graph && Array.isArray(graph.signatures) && graph.signatures.length > 0) {
-            const entries = new Map();
-            entries.set('', graph);
-            for (const signature of graph.signatures) {
-                entries.set(signature.name, signature);
-            }
-            const selector = new view.SelectView(this._view, entries, signature || graph);
-            selector.on('change', (sender, data) => this.emit('update-active-graph-signature', data));
-            this.addEntry('signature', selector);
-        }
         const metadata = model.metadata;
         if (Array.isArray(metadata) && metadata.length > 0) {
             this.addSection('Metadata');
@@ -3636,68 +3683,78 @@ view.ModelSidebar = class extends view.ObjectSidebar {
                 this.addProperty(argument.name, argument.value);
             }
         }
-        if (graph) {
-            const type = graph.type || 'graph';
-            switch (type) {
-                case 'graph':
-                    this.addHeader('Graph Properties');
-                    break;
-                case 'function':
-                    this.addHeader('Function Properties');
-                    break;
-                case 'weights':
-                    this.addHeader('Weights Properties');
-                    break;
-                default:
-                    throw new view.Error(`Unsupported graph type '${type}'.`);
-            }
-            if (graph.name) {
-                this.addProperty('name', graph.name);
-            }
-            if (graph.version) {
-                this.addProperty('version', graph.version);
-            }
-            if (graph.description) {
-                this.addProperty('description', graph.description);
+    }
+};
+
+view.TargetSidebar = class extends view.ObjectSidebar {
+
+    constructor(context, target, signature) {
+        super(context);
+        this._target = target;
+        this._signature = signature;
+    }
+
+    render() {
+        const target = this._target;
+        const signature = this._signature;
+        if (target.name) {
+            const item = this.addProperty('name', target.name);
+            if (target.type === 'function') {
+                item.action('\u0192', 'Show Function Documentation', () => {
+                    this.emit('show-definition', null);
+                });
             }
-            const attributes = signature ? signature.attributes : graph.attributes;
-            const inputs = signature ? signature.inputs : graph.inputs;
-            const outputs = signature ? signature.outputs : graph.outputs;
-            if (Array.isArray(attributes) && attributes.length > 0) {
-                this.addSection('Attributes');
-                for (const attribute of attributes) {
-                    this.addProperty(attribute.name, attribute.value);
-                }
+        }
+        if (signature && signature.name) {
+            this.addProperty('signature', signature.name);
+        }
+        if (target.version) {
+            this.addProperty('version', target.version);
+        }
+        if (target.description) {
+            this.addProperty('description', target.description);
+        }
+        const attributes = signature ? signature.attributes : target.attributes;
+        const inputs = signature ? signature.inputs : target.inputs;
+        const outputs = signature ? signature.outputs : target.outputs;
+        if (Array.isArray(attributes) && attributes.length > 0) {
+            this.addSection('Attributes');
+            for (const attribute of attributes) {
+                this.addProperty(attribute.name, attribute.value);
             }
-            if (Array.isArray(inputs) && inputs.length > 0) {
-                this.addSection('Inputs');
-                for (const input of inputs) {
-                    this.addArgument(input.name, input);
-                }
+        }
+        if (Array.isArray(inputs) && inputs.length > 0) {
+            this.addSection('Inputs');
+            for (const input of inputs) {
+                this.addArgument(input.name, input);
             }
-            if (Array.isArray(outputs) && outputs.length > 0) {
-                this.addSection('Outputs');
-                for (const output of outputs) {
-                    this.addArgument(output.name, output);
-                }
+        }
+        if (Array.isArray(outputs) && outputs.length > 0) {
+            this.addSection('Outputs');
+            for (const output of outputs) {
+                this.addArgument(output.name, output);
             }
-            const metadata = graph.metadata;
-            if (Array.isArray(metadata) && metadata.length > 0) {
-                this.addSection('Metadata');
-                for (const argument of metadata) {
-                    this.addProperty(argument.name, argument.value);
-                }
+        }
+        const metadata = target.metadata;
+        if (Array.isArray(metadata) && metadata.length > 0) {
+            this.addSection('Metadata');
+            for (const argument of metadata) {
+                this.addProperty(argument.name, argument.value);
             }
-            const metrics = graph.metrics;
-            if (Array.isArray(metrics) && metrics.length > 0) {
-                this.addSection('Metrics');
-                for (const argument of metrics) {
-                    this.addProperty(argument.name, argument.value);
-                }
+        }
+        const metrics = target.metrics;
+        if (Array.isArray(metrics) && metrics.length > 0) {
+            this.addSection('Metrics');
+            for (const argument of metrics) {
+                this.addProperty(argument.name, argument.value);
             }
         }
     }
 
+    get identifier() {
+        return 'target';
+    }
+
     addArgument(name, argument) {
         const value = new view.ArgumentView(this._view, argument);
         value.on('focus', (sender, value) => this.emit('focus', value));