From dd4dd938cb173e13443baf44b6a9becb09530444 Mon Sep 17 00:00:00 2001 From: RotPublic <63988781+xiaoyixin-cmd@users.noreply.github.com> Date: Wed, 29 Jun 2022 11:02:37 +0800 Subject: [PATCH] Optimize visual effects Co-authored-by: wuzewu --- frontend/packages/netron/src/style.scss | 306 +++++++++++++++- frontend/packages/netron/src/view-grapher.js | 362 ++++++++++++++++++- 2 files changed, 650 insertions(+), 18 deletions(-) diff --git a/frontend/packages/netron/src/style.scss b/frontend/packages/netron/src/style.scss index 9bb1a827..28312a0f 100644 --- a/frontend/packages/netron/src/style.scss +++ b/frontend/packages/netron/src/style.scss @@ -1,379 +1,681 @@ html { + text-size-adjust: 100%; + text-rendering: optimizeLegibility; + } + body { + overflow: hidden; + margin: 0; + width: 100vw; + height: 100vh; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe WPC', 'Segoe UI', 'Ubuntu', 'Droid Sans', sans-serif, + 'PingFang SC'; font-size: 12px; + text-rendering: geometricPrecision; + background-color: #fff; + + &.dark { + background-color: #1d1d1f; + } } + .graph { + overflow: auto; + width: 100%; + height: 100%; + } + .canvas { + display: block; + // position: absolute; text-rendering: geometricPrecision; + user-select: none; + cursor: grab; + } + path { + stroke: #666; + stroke-width: 1px; + fill: none; + } + line { + stroke: #666; + stroke-width: 1px; + fill: #666; + } + text { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe WPC', 'Segoe UI', 'Ubuntu', 'Droid Sans', sans-serif, + 'PingFang SC'; font-size: 11px; + text-rendering: geometricPrecision; + fill: #000; + + .dark & { + fill: #cfcfd1; + } } + .node-item { + path { + fill: #fff; + fill-opacity: 1; + stroke: none; + transition: fill 0.075s ease-in, fill-opacity 0.075s ease-in; + } + text { + transition: fill 0.075s ease-in; + } + &:hover { + path { + fill: #2932e1; + fill-opacity: 1; + } + text { + fill: #fff; + } } } + .node-item-function path { + fill: #9bb9e8; + fill-opacity: 0.7; + } + .node-item-type { + cursor: pointer; + + path { + fill: #8bb8ff; + fill-opacity: 0.9; + } } + .node-item-type-constant path { + fill: #b4ccb7; + } + .node-item-type-control path { + fill: #a8e9b8; + } + .node-item-type-layer path { + fill: #db989a; + fill-opacity: 0.7; + } + .node-item-type-container path { + fill: #db989a; + fill-opacity: 0.7; + } + .node-item-type-wrapper path { + fill: #6dcde4; + fill-opacity: 0.7; + } + .node-item-type-conv path { + fill: #6dcde4; + fill-opacity: 0.7; + } + .node-item-type-activation path { + fill: #93c2ca; + fill-opacity: 0.7; + } + .node-item-type-pool path { + fill: #de7cce; + fill-opacity: 0.7; + } + .node-item-type-normalization path { + fill: #da96bc; + fill-opacity: 0.7; + } + .node-item-type-dropout path { + fill: #309e51; + fill-opacity: 0.7; + } + .node-item-type-pad path { + fill: #309e51; + fill-opacity: 0.7; + } + .node-item-type-shape path { + fill: #d6c482; + fill-opacity: 0.7; + } + .node-item-type-tensor path { + fill: #6d7ce4; + fill-opacity: 0.7; + } + .node-item-type-transform path { + fill: #cdcb74; + } + .node-item-type-sequence path { + fill: #cdcb74; + } + .node-item-type-data path { + fill: #2576ad; + fill-opacity: 0.7; + } + .node-item-type-custom path { + fill: #e46d6d; + fill-opacity: 0.7; + } + .node-item-input { + cursor: pointer; + + path { + fill: #fff; + } } + .node-item-constant { + cursor: pointer; + + path { + fill: #eee; + } } + .node-item-undefined { + cursor: pointer; + + path { + fill: #ca5353; + fill-opacity: 0.7; + } } + .node-attribute { + cursor: pointer; + + text { + font-size: 9px; + font-weight: normal; + } } + .node-attribute path { + fill: #fff; + stroke-width: 0; + + .dark & { + fill: #262629; + } } + .graph-item-input { + cursor: pointer; + + path { + fill: #e49d6d; + fill-opacity: 0.7; + } } + .graph-item-output { + cursor: pointer; + + path { + fill: #e4e06d; + fill-opacity: 0.9; + } } + .edge-label text { + font-size: 10px; + } + .edge-path { + stroke: #666; + stroke-width: 1px; + fill: none; + } + #arrowhead-vee path { + fill: #666; + } + .edge-path-control-dependency { + stroke-dasharray: 3, 2; + } + .cluster .clusterGroup { + fill: #dce9ff; + stroke: #666; + stroke-width: 1px; + } + .node-item-function path { + fill: #9bb9e8; + fill-opacity: 0.7; + } + .cluster .clusterGroup-constant { + fill: #e8efe9; + } + .cluster .clusterGroup-control { + fill: #e4f8e9; + } + .cluster .clusterGroup-layer { + fill: #f4e0e0; + } + .cluster .clusterGroup-conv { + fill: #d3f0f6; + } + .cluster .clusterGroup-container { + fill: #f4e0e0; + } + .cluster .clusterGroup-wrapper { + fill: #d3f0f6; + } + .cluster .clusterGroup-activation { + fill: #deecef; + } + .cluster .clusterGroup-pool { + fill: #f5d7f0; + } + .cluster .clusterGroup-normalization { + fill: #f3dfea; + } + .cluster .clusterGroup-dropout { + fill: #c0e1ca; + } + .cluster .clusterGroup-pad { + fill: #c0e1ca; + } + .cluster .clusterGroup-shape { + fill: #f2edd9; + } + .cluster .clusterGroup-tensor { + fill: #d3d7f6; + } + .cluster .clusterGroup-transform { + fill: #f0efd5; + } + .cluster .clusterGroup-sequence { + fill: #f0efd5; + } + .cluster .clusterGroup-data { + fill: #bdd5e6; + } + .cluster .clusterGroup-custom { + fill: #f6d3d3; + } + .cluster .clusterButton { - fill-opacity: 0.3; + + fill-opacity: 0; + fill: #db989a; + stroke: #999; + cursor: pointer; + } + .cluster .button-text { + fill: #999; + } + .cluster.border { + display: none; + } + .select { + &.edge-path { + stroke: #1527c2; + stroke-width: 2px; + stroke-dasharray: 6px 3px; + stroke-dashoffset: 0; + animation: pulse 4s infinite linear; + } + .node.border { + stroke: #1527c2; + stroke-width: 2px; + stroke-dasharray: 6px 3px; + stroke-dashoffset: 0; + animation: pulse 4s infinite linear; + } + .cluster.border { + display: block; + stroke: #1527c2; + stroke-width: 2px; + stroke-dasharray: 6px 3px; + stroke-dashoffset: 0; + animation: pulse 4s infinite linear; + } } + @keyframes pulse { + from { + stroke-dashoffset: 100px; + } + to { + stroke-dashoffset: 0; + } -} +} \ No newline at end of file diff --git a/frontend/packages/netron/src/view-grapher.js b/frontend/packages/netron/src/view-grapher.js index d2c1c045..9500be46 100644 --- a/frontend/packages/netron/src/view-grapher.js +++ b/frontend/packages/netron/src/view-grapher.js @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -15,50 +15,71 @@ */ var grapher = grapher || {}; + var dagre = dagre || require('dagre'); grapher.Renderer = class { constructor(host, svgElement, view) { this._document = host.document; + this._svgElement = svgElement; + this._host = host; + this._view = view; } render(graph) { let svgClusterGroup = null; + let svgEdgePathGroup = null; + let svgEdgeLabelGroup = null; + let svgNodeGroup = null; + svgClusterGroup = this.createElement('g'); + svgClusterGroup.setAttribute('id', 'clusters'); + svgClusterGroup.setAttribute('class', 'clusters'); + this._svgElement.appendChild(svgClusterGroup); svgEdgePathGroup = this.createElement('g'); + svgEdgePathGroup.setAttribute('id', 'edge-paths'); + svgEdgePathGroup.setAttribute('class', 'edge-paths'); + this._svgElement.appendChild(svgEdgePathGroup); svgEdgeLabelGroup = this.createElement('g'); + svgEdgeLabelGroup.setAttribute('id', 'edge-labels'); + svgEdgeLabelGroup.setAttribute('class', 'edge-labels'); + this._svgElement.appendChild(svgEdgeLabelGroup); svgNodeGroup = this.createElement('g'); + svgNodeGroup.setAttribute('id', 'nodes'); + svgNodeGroup.setAttribute('class', 'nodes'); + this._svgElement.appendChild(svgNodeGroup); // } else { - // svgClusterGroup = this._document.getElementById('clusters') - // svgEdgePathGroup = this._document.getElementById('edge-paths') - // svgEdgeLabelGroup = this._document.getElementById('edge-labels') - // svgNodeGroup = this._document.getElementById('nodes') + // svgClusterGroup = this._document.getElementById('clusters') + // svgEdgePathGroup = this._document.getElementById('edge-paths') + // svgEdgeLabelGroup = this._document.getElementById('edge-labels') + // svgNodeGroup = this._document.getElementById('nodes') // } for (const nodeId of graph.nodes()) { if (graph.children(nodeId).length == 0) { const node = graph.node(nodeId); + // 在这里进行缓存的判断 // console.log('this._document', this._document); // const nodeDom = this._document.getElementById(node.id); @@ -67,11 +88,15 @@ grapher.Renderer = class { // 这个节点存在过 svgNodeGroup.appendChild(this._view._nodes[node.id]); const nodeBox = this._view._nodes[node.id].getBBox(); + node.width = nodeBox.width; + node.height = nodeBox.height; + node.element = this._view._nodes[node.id]; } else { const element = this.createElement('g'); + if (node.id) { element.setAttribute('id', node.id); } @@ -80,17 +105,25 @@ grapher.Renderer = class { Object.prototype.hasOwnProperty.call(node, 'class') ? 'node ' + node.class : 'node' ); element.style.opacity = 0; + const container = this.createElement('g'); + container.appendChild(node.label); // node.label 就是fromat 之后的节点 element.appendChild(container); svgNodeGroup.appendChild(element); const nodeBox = node.label.getBBox(); + const nodeX = -nodeBox.width / 2; + const nodeY = -nodeBox.height / 2; + container.setAttribute('transform', 'translate(' + nodeX + ',' + nodeY + ')'); + node.width = nodeBox.width; + node.height = nodeBox.height; + node.element = element; } } @@ -98,27 +131,43 @@ grapher.Renderer = class { for (const edgeId of graph.edges()) { const edge = graph.edge(edgeId); + if (edge.label) { const tspan = this.createElement('tspan'); + tspan.setAttribute('xml:space', 'preserve'); + tspan.setAttribute('dy', '1em'); + tspan.setAttribute('x', '1'); + tspan.appendChild(this._document.createTextNode(edge.label)); const text = this.createElement('text'); + text.appendChild(tspan); const textContainer = this.createElement('g'); + textContainer.appendChild(text); const labelElement = this.createElement('g'); + labelElement.style.opacity = 0; + labelElement.setAttribute('class', 'edge-label'); + labelElement.appendChild(textContainer); svgEdgeLabelGroup.appendChild(labelElement); const edgeBox = textContainer.getBBox(); + const edgeX = -edgeBox.width / 2; + const edgeY = -edgeBox.height / 2; + textContainer.setAttribute('transform', 'translate(' + edgeX + ',' + edgeY + ')'); + edge.width = edgeBox.width; + edge.height = edgeBox.height; + edge.labelElement = labelElement; } } @@ -128,49 +177,74 @@ grapher.Renderer = class { for (const nodeId of graph.nodes()) { if (graph.children(nodeId).length == 0) { const node = graph.node(nodeId); + node.element.setAttribute('transform', 'translate(' + node.x + ',' + node.y + ')'); + node.element.style.opacity = 1; + delete node.element; } } for (const edgeId of graph.edges()) { const edge = graph.edge(edgeId); + if (edge.labelElement) { edge.labelElement.setAttribute('transform', 'translate(' + edge.x + ',' + edge.y + ')'); + edge.labelElement.style.opacity = 1; + delete edge.labelElement; } } const edgePathGroupDefs = this.createElement('defs'); + svgEdgePathGroup.appendChild(edgePathGroupDefs); const marker = this.createElement('marker'); + marker.setAttribute('id', 'arrowhead-vee'); + marker.setAttribute('viewBox', '0 0 10 10'); + marker.setAttribute('refX', 9); + marker.setAttribute('refY', 5); + marker.setAttribute('markerUnits', 'strokeWidth'); + marker.setAttribute('markerWidth', 8); + marker.setAttribute('markerHeight', 6); + marker.setAttribute('orient', 'auto'); + edgePathGroupDefs.appendChild(marker); const markerPath = this.createElement('path'); + markerPath.setAttribute('d', 'M 0 0 L 10 5 L 0 10 L 4 5 z'); + markerPath.style.setProperty('stroke-width', 1); + markerPath.style.setProperty('stroke-dasharray', '1,0'); + marker.appendChild(markerPath); for (const edgeId of graph.edges()) { const edge = graph.edge(edgeId); + const edgePath = grapher.Renderer._computeCurvePath(edge, graph.node(edgeId.v), graph.node(edgeId.w)); + const edgeElement = this.createElement('path'); + edgeElement.setAttribute( 'class', Object.prototype.hasOwnProperty.call(edge, 'class') ? 'edge-path ' + edge.class : 'edge-path' ); edgeElement.setAttribute('d', edgePath); + edgeElement.setAttribute('marker-end', 'url(#arrowhead-vee)'); + if (edge.id) { edgeElement.setAttribute('id', edge.id); } @@ -183,6 +257,7 @@ grapher.Renderer = class { svgEdgePathGroup.appendChild(edgeElement); } const groupArray = []; + for (const nodeId of graph.nodes()) { if (!Number(nodeId) && Number(nodeId) !== 0) { groupArray.push(nodeId); @@ -190,52 +265,76 @@ grapher.Renderer = class { } const newGroupArray = groupArray.sort((a, b) => { let level1 = a.split('/').length; + let level2 = b.split('/').length; + return level1 - level2; }); for (const nodeId of newGroupArray) { if (graph.children(nodeId).length > 0) { const node = graph.node(nodeId); - // const nodeDom = this._document.getElementById(`node-${nodeId}`) - // if (this._view._nodes.hasOwnProperty(node.id)) { - // // 这个节点存在过 - // svgNodeGroup.appendChild(this._view._nodes[node.id]); - // const nodeBox = this._view._nodes[node.id].getBBox(); - // node.width = nodeBox.width; - // node.height = nodeBox.height; - // node.element = this._view._nodes[node.id] + if (this._view._clusters.hasOwnProperty(node.id)) { const nodeDom = this._view._clusters.hasOwnProperty(node.id); + nodeDom.setAttribute('transform', 'translate(' + node.x + ',' + node.y + ')'); + nodeDom.firstChild.setAttribute('x', -node.width / 2); + nodeDom.firstChild.setAttribute('y', -node.height / 2); + nodeDom.firstChild.setAttribute('width', node.width + 10); + nodeDom.firstChild.setAttribute('height', node.height + 10); } else { const nodeElement = this.createElement('g'); + nodeElement.setAttribute('class', 'cluster'); + nodeElement.setAttribute('id', `node-${nodeId}`); + nodeElement.setAttribute('transform', 'translate(' + node.x + ',' + node.y + ')'); + const rect = this.createElement('rect'); + const tspan = this.createElement('tspan'); + const button = this.createElement('circle'); + const buttonSign = this.createElement('tspan'); + button.setAttribute('r', '6.5'); + button.setAttribute('cx', node.width / 2 - 20 + 7.5 + 10); + button.setAttribute('cy', -(node.height / 2) + 5 + 7.5); + buttonSign.setAttribute('x', node.width / 2 - 15 + 9); + buttonSign.setAttribute('y', -(node.height / 2) + 1.3); + buttonSign.setAttribute('xml:space', 'preserve'); + buttonSign.setAttribute('dy', '1em'); + buttonSign.setAttribute('font-size', '16px'); + buttonSign.setAttribute('class', 'button-text'); + button.setAttribute('class', 'clusterButton'); + tspan.setAttribute('xml:space', 'preserve'); + tspan.setAttribute('dy', '1em'); + tspan.setAttribute('x', 0); + tspan.setAttribute('y', -(node.height / 2) + 5); + tspan.setAttribute('text-anchor', 'middle'); + let name = ''; + for (const nodes of this._host._view._allGraph.nodes) { if (nodes.name === node.nodeId) { name = nodes.show_name.split('/')[nodes.show_name.split('/').length - 1]; @@ -244,23 +343,36 @@ grapher.Renderer = class { tspan.appendChild(this._document.createTextNode(name)); buttonSign.appendChild(this._document.createTextNode('-')); const text = this.createElement('text'); + text.appendChild(tspan); const text2 = this.createElement('text'); + text2.appendChild(buttonSign); rect.setAttribute('class', node.classList.join(' ')); + rect.setAttribute('x', -node.width / 2); + rect.setAttribute('y', -node.height / 2); + rect.setAttribute('width', node.width + 10); + rect.setAttribute('height', node.height + 10); + const borderElement = this.createElement('path'); + borderElement.setAttribute('class', ['cluster', 'border'].join(' ')); + borderElement.setAttribute( 'd', - grapher.NodeElement.roundedRect( + grapher.NodeElement.roundedRect2( -node.width / 2, + -node.height / 2, + node.width + 10, + node.height + 10, + true, true, true, @@ -270,19 +382,25 @@ grapher.Renderer = class { nodeElement.addEventListener('click', () => { this._view.select({ id: `node-${nodeId}`, + name: nodeId, + type: 'node' }); }); text2.addEventListener('click', () => { this._host.selectNodeId({ nodeId: node.nodeId, + expand: node.expand, + isKeepData: node.isKeepData }); this._host.selectItems({ id: `node-${node.nodeId}`, + name: node.nodeId, + type: 'node' }); }); @@ -340,17 +458,23 @@ grapher.Renderer = class { static _computeCurvePath(edge, tail, head) { const points = edge.points.slice(1, edge.points.length - 1); + points.unshift(grapher.Renderer.intersectRect(tail, points[0])); + points.push(grapher.Renderer.intersectRect(head, points[points.length - 1])); const path = new Path(); + const curve = new Curve(path); + for (let i = 0; i < points.length; i++) { const point = points[i]; + if (i == 0) { curve.lineStart(); } curve.point(point.x, point.y); + if (i == points.length - 1) { curve.lineEnd(); } @@ -361,24 +485,34 @@ grapher.Renderer = class { static intersectRect(node, point) { const x = node.x; + const y = node.y; + const dx = point.x - x; + const dy = point.y - y; + let w = node.width / 2; + let h = node.height / 2; + let sx; + let sy; + if (Math.abs(dy) * w > Math.abs(dx) * h) { if (dy < 0) { h = -h; } sx = dy === 0 ? 0 : (h * dx) / dy; + sy = h; } else { if (dx < 0) { w = -w; } sx = w; + sy = dx === 0 ? 0 : (w * dy) / dx; } return {x: x + sx, y: y + sy}; @@ -388,17 +522,21 @@ grapher.Renderer = class { grapher.NodeElement = class { constructor(document) { this._document = document; + this._blocks = []; } block(type) { this._block = null; + switch (type) { case 'header': this._block = new grapher.NodeElement.Header(this._document); + break; case 'list': this._block = new grapher.NodeElement.List(this._document); + break; } this._blocks.push(this._block); @@ -407,9 +545,12 @@ grapher.NodeElement = class { format(contextElement) { const rootElement = this.createElement('g'); + contextElement.appendChild(rootElement); let width = 0; + let height = 0; + const tops = []; for (const block of this._blocks) { @@ -424,22 +565,91 @@ grapher.NodeElement = class { for (let i = 0; i < this._blocks.length; i++) { // push 进来的header 或者 list const top = tops.shift(); + this._blocks[i].update(rootElement, top, width, i == 0, i == this._blocks.length - 1); } const borderElement = this.createElement('path'); + borderElement.setAttribute('class', ['node', 'border'].join(' ')); + borderElement.setAttribute('d', grapher.NodeElement.roundedRect(0, 0, width, height, true, true, true, true)); + rootElement.appendChild(borderElement); contextElement.innerHTML = ''; + return rootElement; } static roundedRect(x, y, width, height, r1, r2, r3, r4) { const 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' + ); + } + static roundedRect2(x, y, width, height, r1, r2, r3, r4) { + const radius = 10; + r1 = r1 ? radius : 0; + r2 = r2 ? radius : 0; + r3 = r3 ? radius : 0; + r4 = r4 ? radius : 0; + return ( 'M' + (x + r1) + @@ -497,39 +707,56 @@ grapher.NodeElement = class { grapher.NodeElement.Header = class { constructor(document) { this._document = document; + this._items = []; } add(id, classList, content, tooltip, handler) { this._items.push({ id: id, + classList: classList, + content: content, + tooltip: tooltip, + handler: handler }); } layout(parentElement) { this._width = 0; + this._height = 0; + this._elements = []; + let x = 0; + const y = 0; + for (const item of this._items) { const yPadding = 4; + const xPadding = 7; + const element = this.createElement('g'); + let 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 = classList.concat(item.classList); } element.setAttribute('class', classList.join(' ')); + if (item.id) { element.setAttribute('id', item.id); } @@ -538,27 +765,41 @@ grapher.NodeElement.Header = class { } if (item.tooltip) { const titleElement = this.createElement('title'); + titleElement.textContent = item.tooltip; + element.appendChild(titleElement); } if (item.content) { textElement.textContent = item.content; } 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; } @@ -578,15 +819,19 @@ grapher.NodeElement.Header = class { update(parentElement, top, width, first, last) { const dx = width - this._width; + let i; + let element; for (i = 0; i < this._elements.length; i++) { element = this._elements[i]; + if (i == 0) { element.width = element.width + dx; } else { element.x = element.x + dx; + element.tx = element.tx + dx; } element.y = element.y + top; @@ -594,40 +839,61 @@ grapher.NodeElement.Header = class { 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.NodeElement.roundedRect(0, 0, element.width, element.height, r1, r2, r3, r4) ); - element.text.setAttribute('x', 6); - element.text.setAttribute('y', element.ty); + element.text.setAttribute('x', 7); + + element.text.setAttribute('y', element.ty - 1); } 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); } } 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); } } @@ -640,6 +906,7 @@ grapher.NodeElement.Header = class { grapher.NodeElement.List = class { constructor(document) { this._document = document; + this._items = []; } @@ -657,49 +924,72 @@ grapher.NodeElement.List = class { layout(parentElement) { 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; + for (const item of this._items) { const yPadding = 1; + const xPadding = 6; + const textElement = this.createElement('text'); + if (item.id) { textElement.setAttribute('id', item.id); } textElement.setAttribute('xml:space', 'preserve'); + this._element.appendChild(textElement); if (item.tooltip) { const titleElement = this.createElement('title'); + titleElement.textContent = item.tooltip; + textElement.appendChild(titleElement); } const textNameElement = this.createElement('tspan'); + textNameElement.textContent = item.name; + if (item.separator.trim() != '=') { textNameElement.style.fontWeight = 'bold'; } textElement.appendChild(textNameElement); const textValueElement = this.createElement('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; } textElement.setAttribute('x', x + xPadding); + textElement.setAttribute('y', this._height + yPadding - size.y); + this._height += yPadding + size.height + yPadding; } this._height += 3; @@ -721,9 +1011,13 @@ grapher.NodeElement.List = class { this._element.setAttribute('transform', 'translate(0,' + top + ')'); const r1 = first; + const r2 = first; + const r3 = last; + const r4 = last; + this._backgroundElement.setAttribute( 'd', grapher.NodeElement.roundedRect(0, 0, width, this._height, r1, r2, r3, r4) @@ -731,11 +1025,17 @@ grapher.NodeElement.List = class { if (!first) { const lineElement = this.createElement('line'); + lineElement.setAttribute('class', 'node'); + lineElement.setAttribute('x1', 0); + lineElement.setAttribute('x2', width); + lineElement.setAttribute('y1', 0); + lineElement.setAttribute('y2', 0); + this._element.appendChild(lineElement); } } @@ -748,9 +1048,13 @@ grapher.NodeElement.List = class { class Path { constructor() { this._x0 = null; + this._y0 = null; + this._x1 = null; + this._y1 = null; + this._data = ''; } @@ -769,7 +1073,9 @@ class Path { closePath() { if (this._x1 !== null) { this._x1 = this._x0; + this._y1 = this._y0; + this._data += 'Z'; } } @@ -786,9 +1092,13 @@ class Curve { lineStart() { this._x0 = NaN; + this._x1 = NaN; + this._y0 = NaN; + this._y1 = NaN; + this._point = 0; } @@ -796,10 +1106,13 @@ class Curve { switch (this._point) { case 3: this.curve(this._x1, this._y1); + this._context.lineTo(this._x1, this._y1); + break; case 2: this._context.lineTo(this._x1, this._y1); + break; } if (this._line || (this._line !== 0 && this._point === 1)) { @@ -810,10 +1123,13 @@ class Curve { point(x, y) { x = +x; + y = +y; + switch (this._point) { case 0: this._point = 1; + if (this._line) { this._context.lineTo(x, y); } else { @@ -822,29 +1138,42 @@ class Curve { break; case 1: this._point = 2; + break; case 2: this._point = 3; + this._context.lineTo((5 * this._x0 + this._x1) / 6, (5 * this._y0 + this._y1) / 6); + this.curve(x, y); + break; default: this.curve(x, y); + break; } this._x0 = this._x1; + this._x1 = x; + this._y0 = this._y1; + this._y1 = y; } curve(x, y) { this._context.bezierCurveTo( (2 * this._x0 + this._x1) / 3, + (2 * this._y0 + this._y1) / 3, + (this._x0 + 2 * this._x1) / 3, + (this._y0 + 2 * this._y1) / 3, + (this._x0 + 4 * this._x1 + x) / 6, + (this._y0 + 4 * this._y1 + y) / 6 ); } @@ -852,5 +1181,6 @@ class Curve { if (typeof module !== 'undefined' && typeof module.exports === 'object') { module.exports.Renderer = grapher.Renderer; + module.exports.NodeElement = grapher.NodeElement; } -- GitLab