diff --git a/src/frontend/core/datas/Array1DData.js b/src/frontend/core/datas/Array1DData.js index 6b7fb5ec3d537ebe05e3399d52520ba13dd360a4..44752c8c8cf0126f54921455649b67cbe7295fc8 100644 --- a/src/frontend/core/datas/Array1DData.js +++ b/src/frontend/core/datas/Array1DData.js @@ -7,14 +7,10 @@ class Array1DData extends Array2DData { this.chartData = null; } - render() { - super.render(); - this.syncChartData(); - } - set(array1d = []) { const array2d = [array1d]; super.set(array2d); + this.syncChartData(); } notify(x, v) { @@ -39,10 +35,7 @@ class Array1DData extends Array2DData { } syncChartData() { - if (this.chartData) { - this.chartData.data = this.data; - this.chartData.render(); - } + if (this.chartData) this.chartData.data = this.data; } } diff --git a/src/frontend/core/datas/Array2DData.js b/src/frontend/core/datas/Array2DData.js index a2feeeb9d1e290b4531756ed8ff1dca96752bad6..9ff3802db422c0f112b8640b8bce9c557860a7dd 100644 --- a/src/frontend/core/datas/Array2DData.js +++ b/src/frontend/core/datas/Array2DData.js @@ -21,12 +21,10 @@ class Array2DData extends Data { notify(x, y, v = this.data[x][y].value) { this.data[x][y].value = v; this.data[x][y].notified = true; - this.render(); } denotify(x, y) { this.data[x][y].notified = false; - this.render(); } select(sx, sy, ex = sx, ey = sy) { @@ -35,7 +33,6 @@ class Array2DData extends Data { this.data[x][y].selected = true; } } - this.render(); } selectRow(x, sy, ey) { @@ -52,7 +49,6 @@ class Array2DData extends Data { this.data[x][y].selected = false; } } - this.render(); } deselectRow(x, sy, ey) { diff --git a/src/frontend/core/datas/Data.js b/src/frontend/core/datas/Data.js index 765224f0806d9e796130e2af8328bcc1693403a1..f6982cb1d946037706777484123193d3754cc7d1 100644 --- a/src/frontend/core/datas/Data.js +++ b/src/frontend/core/datas/Data.js @@ -29,7 +29,6 @@ class Data { } set() { - this.render(); } wait() { diff --git a/src/frontend/core/datas/GraphData.js b/src/frontend/core/datas/GraphData.js index c575fe00301ff03f7b017866435c0577b4c7301b..819c8fd3d9bfd2d8bcb11618f9931001b8fbc5c6 100644 --- a/src/frontend/core/datas/GraphData.js +++ b/src/frontend/core/datas/GraphData.js @@ -1,5 +1,4 @@ import { Data } from '/core/datas'; -import { GraphTracer } from '/core/tracers'; import { distance } from '/common/util'; import { tracerManager } from '/core'; @@ -24,105 +23,48 @@ class GraphData extends Data { edgeWeightGap: 4, }; this.logData = null; + this.callLayout = { method: this.layoutCircle, args: [] }; } - set(array2d = [], layout = GraphTracer.LAYOUT.CIRCLE, root = 0) { - const { directed, weighted } = this.options; - const { baseWidth, baseHeight, padding } = this.dimensions; - this.graph = new Graph([], [], directed); + set(array2d = []) { + const { weighted } = this.options; + this.nodes = []; + this.edges = []; for (let i = 0; i < array2d.length; i++) { - const id = i; - const weight = null; - const visitedCount = 0; - this.graph.addNode(id, weight, visitedCount); + this.addNode(i); for (let j = 0; j < array2d.length; j++) { const value = array2d[i][j]; if (value) { - const source = i; - const target = j; - const weight = weighted ? value : null; - const visitedCount = 0; - this.graph.addEdge(source, target, weight, visitedCount); + this.addEdge(i, j, weighted ? value : null); } } } - const left = -baseWidth / 2 + padding; - const top = -baseHeight / 2 + padding; - const right = baseWidth / 2 - padding; - const bottom = baseHeight / 2 - padding; - const width = right - left; - const height = bottom - top; - const rect = { left, top, right, bottom, width, height }; - switch (layout) { - case GraphTracer.LAYOUT.CIRCLE: - this.graph.layoutCircle(rect); - break; - case GraphTracer.LAYOUT.TREE: - this.graph.layoutTree(rect, root); - break; - case GraphTracer.LAYOUT.RANDOM: - this.graph.layoutRandom(rect); - break; - case GraphTracer.LAYOUT.NONE: - default: - break; - } + this.layout(); super.set(); } - visit(target, source, weight) { - this.visitOrLeave(target, source, weight, true); - } - - leave(target, source, weight) { - this.visitOrLeave(target, source, weight, false); + addNode(id, weight = null, visitedCount = 0, selectedCount = 0, x = 0, y = 0) { + if (this.findNode(id)) return; + this.nodes.push({ id, weight, visitedCount, selectedCount, x, y }); + this.layout(); } - visitOrLeave(target, source, weight, visit) { - const edge = this.graph.findEdge(source, target); - if (edge) edge.visitedCount += visit ? 1 : -1; - const node = this.graph.findNode(target); - node.weight = weight; - node.visitedCount += visit ? 1 : -1; - this.render(); - if (this.logData) { - this.logData.print(visit ? (source || '') + ' -> ' + target : (source || '') + ' <- ' + target); - } + addEdge(source, target, weight = null, visitedCount = 0, selectedCount = 0) { + if (this.findEdge(source, target)) return; + this.edges.push({ source, target, weight, visitedCount, selectedCount }); + this.layout(); } updateNode(id, update) { - const node = this.graph.findNode(id); + const node = this.findNode(id); Object.assign(node, update); - this.render(); - } - - log(tracerKey) { - this.logData = tracerKey ? tracerManager.datas[tracerKey] : null; - } -} - -class Graph { - constructor(nodes, edges, directed) { - this.nodes = nodes; - this.edges = edges; - this.directed = directed; - } - - addNode(id, weight, visitedCount, x = 0, y = 0) { - if (this.findNode(id)) return; - this.nodes.push({ id, weight, visitedCount, x, y }); - } - - addEdge(source, target, weight, visitedCount) { - if (this.findEdge(source, target)) return; - this.edges.push({ source, target, weight, visitedCount }); } findNode(id) { return this.nodes.find(node => node.id === id); } - findEdge(source, target, directed = this.directed) { + findEdge(source, target, directed = this.options.directed) { if (directed) { return this.edges.find(edge => edge.source === source && edge.target === target); } else { @@ -132,7 +74,7 @@ class Graph { } } - findLinkedEdges(source, directed = this.directed) { + findLinkedEdges(source, directed = this.options.directed) { if (directed) { return this.edges.filter(edge => edge.source === source); } else { @@ -140,17 +82,35 @@ class Graph { } } - findLinkedNodeIds(source, directed = this.directed) { + findLinkedNodeIds(source, directed = this.options.directed) { const edges = this.findLinkedEdges(source, directed); return edges.map(edge => edge.source === source ? edge.target : edge.source); } - findLinkedNodes(source, directed = this.directed) { + findLinkedNodes(source, directed = this.options.directed) { const ids = this.findLinkedNodeIds(source, directed); return ids.map(id => this.findNode(id)); } - layoutCircle(rect) { + getRect() { + const { baseWidth, baseHeight, padding } = this.dimensions; + const left = -baseWidth / 2 + padding; + const top = -baseHeight / 2 + padding; + const right = baseWidth / 2 - padding; + const bottom = baseHeight / 2 - padding; + const width = right - left; + const height = bottom - top; + return { left, top, right, bottom, width, height }; + } + + layout() { + const { method, args } = this.callLayout; + method.apply(this, args); + } + + layoutCircle() { + this.callLayout = { method: this.layoutCircle, args: arguments }; + const rect = this.getRect(); const unitAngle = 2 * Math.PI / this.nodes.length; let angle = -Math.PI / 2; for (const node of this.nodes) { @@ -162,7 +122,17 @@ class Graph { } } - layoutTree(rect, root) { + layoutTree(root = 0, sorted = false) { + this.callLayout = { method: this.layoutTree, args: arguments }; + const rect = this.getRect(); + + if (this.nodes.length === 1) { + const [node] = this.nodes; + node.x = (rect.left + rect.right) / 2; + node.y = (rect.top + rect.bottom) / 2; + return; + } + let maxDepth = 0; const leafCounts = {}; let marked = {}; @@ -188,6 +158,7 @@ class Graph { node.x = rect.left + (h + leafCounts[node.id] / 2) * hGap; node.y = rect.top + v * vGap; const linkedNodes = this.findLinkedNodes(node.id, false); + if (sorted) linkedNodes.sort((a, b) => a.id - b.id); for (const linkedNode of linkedNodes) { if (marked[linkedNode.id]) continue; recursivePosition(linkedNode, h, v + 1); @@ -198,7 +169,9 @@ class Graph { recursivePosition(rootNode, 0, 0); } - layoutRandom(rect) { + layoutRandom() { + this.callLayout = { method: this.layoutRandom, args: arguments }; + const rect = this.getRect(); const placedNodes = []; for (const node of this.nodes) { do { @@ -208,6 +181,47 @@ class Graph { placedNodes.push(node); } } + + visit(target, source, weight) { + this.visitOrLeave(target, source, weight, true); + } + + leave(target, source, weight) { + this.visitOrLeave(target, source, weight, false); + } + + visitOrLeave(target, source, weight, visit) { + const edge = this.findEdge(source, target); + if (edge) edge.visitedCount += visit ? 1 : -1; + const node = this.findNode(target); + node.weight = weight; + node.visitedCount += visit ? 1 : -1; + if (this.logData) { + this.logData.print(visit ? (source || '') + ' -> ' + target : (source || '') + ' <- ' + target); + } + } + + select(target, source) { + this.selectOrDeselect(target, source, true); + } + + deselect(target, source) { + this.selectOrDeselect(target, source, false); + } + + selectOrDeselect(target, source, select) { + const edge = this.findEdge(source, target); + if (edge) edge.selectedCount += select ? 1 : -1; + const node = this.findNode(target); + node.selectedCount += select ? 1 : -1; + if (this.logData) { + this.logData.print(select ? (source || '') + ' => ' + target : (source || '') + ' <= ' + target); + } + } + + log(tracerKey) { + this.logData = tracerKey ? tracerManager.datas[tracerKey] : null; + } } export default GraphData; \ No newline at end of file diff --git a/src/frontend/core/datas/LogData.js b/src/frontend/core/datas/LogData.js index 44d1f9ae16a0a2a457f398725d8b7ffed7629013..98bca09392e7f01145253f6476f8d7d5ce13291e 100644 --- a/src/frontend/core/datas/LogData.js +++ b/src/frontend/core/datas/LogData.js @@ -8,7 +8,6 @@ class LogData extends Data { print(message) { this.messages.push(message); - this.render(); } } diff --git a/src/frontend/core/renderers/GraphRenderer/index.jsx b/src/frontend/core/renderers/GraphRenderer/index.jsx index 402c0eeee87a782a72a77f6fd2aef1249f6aace0..417bec7280f71a10882332edc995733d4dd3c5b7 100644 --- a/src/frontend/core/renderers/GraphRenderer/index.jsx +++ b/src/frontend/core/renderers/GraphRenderer/index.jsx @@ -14,15 +14,16 @@ class GraphRenderer extends Renderer { handleMouseDown(e) { super.handleMouseDown(e); const coords = this.computeCoords(e); - const { graph, dimensions } = this.props.data; + const { nodes, dimensions } = this.props.data; const { nodeRadius } = dimensions; - this.selectedNode = graph.nodes.find(node => distance(coords, node) <= nodeRadius); + this.selectedNode = nodes.find(node => distance(coords, node) <= nodeRadius); } handleMouseMove(e) { if (this.selectedNode) { const coords = this.computeCoords(e); this.props.data.updateNode(this.selectedNode.id, coords); + this.refresh(); } else { super.handleMouseMove(e); } @@ -38,7 +39,7 @@ class GraphRenderer extends Renderer { } renderData() { - const { graph, options, dimensions } = this.props.data; + const { nodes, edges, options, dimensions } = this.props.data; const { baseWidth, baseHeight, nodeRadius, arrowGap, nodeWeightGap, edgeWeightGap } = dimensions; const { directed, weighted } = options; const viewBox = [ @@ -53,15 +54,18 @@ class GraphRenderer extends Renderer { + + + { - graph.edges.sort((a, b) => a.visitedCount - b.visitedCount).map(edge => { - const { source, target, weight, visitedCount } = edge; - const { x: sx, y: sy } = graph.findNode(source); - let { x: ex, y: ey } = graph.findNode(target); + edges.sort((a, b) => a.visitedCount - b.visitedCount).map(edge => { + const { source, target, weight, visitedCount, selectedCount } = edge; + const { x: sx, y: sy } = this.props.data.findNode(source); + let { x: ex, y: ey } = this.props.data.findNode(target); const mx = (sx + ex) / 2; const my = (sy + ey) / 2; const dx = ex - sx; @@ -76,7 +80,7 @@ class GraphRenderer extends Renderer { } return ( - + { weighted && @@ -90,10 +94,10 @@ class GraphRenderer extends Renderer { }) } { - graph.nodes.map(node => { - const { id, x, y, weight, visitedCount } = node; + nodes.map(node => { + const { id, x, y, weight, visitedCount, selectedCount } = node; return ( - {id} diff --git a/src/frontend/core/renderers/GraphRenderer/stylesheet.scss b/src/frontend/core/renderers/GraphRenderer/stylesheet.scss index a4a2f901c1bd0baef2e772d02cb1bb31da7e2ca6..d4f2331c5a766e5e676f6a8fe7495ce52562290c 100644 --- a/src/frontend/core/renderers/GraphRenderer/stylesheet.scss +++ b/src/frontend/core/renderers/GraphRenderer/stylesheet.scss @@ -24,6 +24,13 @@ text-anchor: left; } + &.selected { + .circle { + fill: $color-selected; + stroke: $color-selected; + } + } + &.visited { .circle { fill: $color-notified; @@ -48,6 +55,20 @@ text-anchor: middle; } + &.selected { + .line { + stroke: $color-selected; + + &.directed { + marker-end: url(#markerArrowSelected); + } + } + + .weight { + fill: $color-selected; + } + } + &.visited { .line { stroke: $color-notified; @@ -66,6 +87,10 @@ .arrow { fill: $color-font; + &.selected { + fill: $color-selected; + } + &.visited { fill: $color-notified; } diff --git a/src/frontend/core/renderers/Renderer/index.jsx b/src/frontend/core/renderers/Renderer/index.jsx index 885062f48da3b54e34ff30687b40321ffd94884f..6ef937ee73eb2cb54b8e55162a6c5aeb69d906a6 100644 --- a/src/frontend/core/renderers/Renderer/index.jsx +++ b/src/frontend/core/renderers/Renderer/index.jsx @@ -54,12 +54,6 @@ class Renderer extends React.Component { data.setOnRender(null); } - handleDrag(dx, dy) { - this.centerX -= dx; - this.centerY -= dy; - this.refresh(); - } - handleMouseDown(e) { const { clientX, clientY } = e; this.lastX = clientX; @@ -72,7 +66,9 @@ class Renderer extends React.Component { const { clientX, clientY } = e; const dx = clientX - this.lastX; const dy = clientY - this.lastY; - this.handleDrag(dx, dy); + this.centerX -= dx; + this.centerY -= dy; + this.refresh(); this.lastX = clientX; this.lastY = clientY; } diff --git a/src/frontend/core/tracerManager.jsx b/src/frontend/core/tracerManager.jsx index 767ca53ef3517f7433cdd5ba657de8cbe068f6e3..afa5c24698a0694545d8b3012ec95752e05bd775 100644 --- a/src/frontend/core/tracerManager.jsx +++ b/src/frontend/core/tracerManager.jsx @@ -52,6 +52,7 @@ class TracerManager { } render() { + Object.values(this.datas).forEach(data => data.render()); if (this.onRender) this.onRender(this.renderers); } @@ -166,7 +167,7 @@ class TracerManager { } } - sandboxEval(code){ + sandboxEval(code) { const require = moduleName => window.modules[moduleName]; // fake require eval(code); } diff --git a/src/frontend/core/tracers/GraphTracer.js b/src/frontend/core/tracers/GraphTracer.js index feda89223aee6a4ae48eabfe34cdf59bc1cfd96c..206020df5c78f9cb14c2a8259e225098723317c3 100644 --- a/src/frontend/core/tracers/GraphTracer.js +++ b/src/frontend/core/tracers/GraphTracer.js @@ -5,19 +5,18 @@ class GraphTracer extends Tracer { super(title, options); this.register( + 'addNode', + 'addEdge', + 'layoutCircle', + 'layoutTree', + 'layoutRandom', 'visit', 'leave', + 'select', + 'deselect', 'log', ); } } -GraphTracer.LAYOUT = { - CIRCLE: 'circle', - TREE: 'tree', - RANDOM: 'random', - NONE: 'none', - // FORCE_DIRECTED: 'force_directed', -}; - export default GraphTracer; \ No newline at end of file