diff --git a/.jshintrc b/.jshintrc index 4eeae00..b62fc40 100644 --- a/.jshintrc +++ b/.jshintrc @@ -13,6 +13,7 @@ "beforeEach": false, "it": false, "expect": false, + "spyOn": false, "TweenLite": false, "Linear": false, "Elastic": false, diff --git a/src/edge/edge.html b/src/edge/edge.html index 296c2d8..abfcbb5 100644 --- a/src/edge/edge.html +++ b/src/edge/edge.html @@ -1,15 +1,25 @@ diff --git a/src/edge/edge.js b/src/edge/edge.js index 00d7239..82bc666 100644 --- a/src/edge/edge.js +++ b/src/edge/edge.js @@ -1,14 +1,20 @@ import {inject, customElement, bindable, containerless} from 'aurelia-framework'; +import {EventAggregator} from 'aurelia-event-aggregator'; import $ from 'jquery'; import 'npm:gsap@1.18.0/src/minified/TweenMax.min.js'; import {EdgeStyle} from './edge-style'; const EDGE_ANIMATE_DURATION = 0.5; const EDGE_ANIMATE_EASE = Power1.easeIn; +const EDGE_ANIMATE_DELAY = 1; + +const HIGHLIGHT_COLOR_SOURCE = 'orange'; +const HIGHLIGHT_COLOR_TARGET = 'rgb(60, 234, 245)'; +const HIGHLIGHT_WIDTH = 5; @customElement('edge') @containerless -@inject(Element, EdgeStyle) +@inject(Element, EventAggregator, EdgeStyle) export class Edge { @bindable data; @bindable sourcex; @@ -16,8 +22,9 @@ export class Edge { @bindable targetx; @bindable targety; - constructor(element, edgeStyle) { + constructor(element, eventAggregator, edgeStyle) { this.element = element; + this.eventAggregator = eventAggregator; this.edgeStyle = edgeStyle; } @@ -27,6 +34,16 @@ export class Edge { } } + get edgePathId() { + return `#edge-path-${this.displayEdge.source.id}-${this.displayEdge.target.id}`; + } + + // FIXME handle upside down https://github.com/CANVE/canve-viz/issues/54 + get edgePathForText() { + return `M${this.displayEdge.source.x} ${this.displayEdge.source.y} L${this.displayEdge.target.x} ${this.displayEdge.target.y}`; + } + + // FIXME Now that have switched from line to path, redo animation to animate path, GSAP may have plugin for this attached() { // Selector this.$edge = $(`#edge-${this.displayEdge.source.id}-${this.displayEdge.target.id}`); @@ -34,16 +51,18 @@ export class Edge { // Animate edge target from source point TweenLite.from(this.$edge[0], EDGE_ANIMATE_DURATION, { attr: {x2: this.displayEdge.source.x, y2: this.displayEdge.source.y}, - ease: EDGE_ANIMATE_EASE + ease: EDGE_ANIMATE_EASE, + delay: EDGE_ANIMATE_DELAY }); + this.registerEvents(); } sourcexChanged(newVal, oldVal) { if (newVal && oldVal) { TweenLite.from(this.$edge[0], EDGE_ANIMATE_DURATION, { attr: {x1: oldVal}, - ease: EDGE_ANIMATE_EASE, + ease: EDGE_ANIMATE_EASE }); } } @@ -52,7 +71,7 @@ export class Edge { if (newVal && oldVal) { TweenLite.from(this.$edge[0], EDGE_ANIMATE_DURATION, { attr: {y1: oldVal}, - ease: EDGE_ANIMATE_EASE, + ease: EDGE_ANIMATE_EASE }); } } @@ -83,13 +102,54 @@ export class Edge { return this.edgeStyle.strokeDash(this.displayEdge.edgeKind); } - edgeColor() { - return this.edgeStyle.strokeColor(this.displayEdge.edgeKind); + get edgeColor() { + if (this.displayEdge.highlightSource) { + return HIGHLIGHT_COLOR_SOURCE; + } else if (this.displayEdge.highlightTarget) { + return HIGHLIGHT_COLOR_TARGET; + } else { + return this.edgeStyle.strokeColor(this.displayEdge.edgeKind); + } + } + + get edgeWidth() { + if (this.displayEdge.highlightSource || this.displayEdge.highlightTarget) { + return HIGHLIGHT_WIDTH; + } else { + return 1; + } + } + + registerEvents() { + this.nodeHoverInSub = this.eventAggregator.subscribe('node.hover.in', this.highlightEdges.bind(this)); + this.nodeHoverOutSub = this.eventAggregator.subscribe('node.hover.out', this.unHighlightEdges.bind(this)); + } + + highlightEdges(node) { + if (this.displayEdge.source.id === node.id) { + this.displayEdge.highlightSource = true; + } + if (this.displayEdge.target.id === node.id) { + this.displayEdge.highlightTarget = true; + } + } + + unHighlightEdges(node) { + if (this.displayEdge.source.id === node.id) { + this.displayEdge.highlightSource = false; + } + if (this.displayEdge.target.id === node.id) { + this.displayEdge.highlightTarget = false; + } + } + + detached() { + this.deregisterEvents(); } - // This may vary with highlight status? - edgeWidth() { - return 1; + deregisterEvents() { + this.nodeHoverInSub.dispose(); + this.nodeHoverOutSub.dispose(); } } diff --git a/src/node/node.js b/src/node/node.js index 16fc177..d447615 100644 --- a/src/node/node.js +++ b/src/node/node.js @@ -1,5 +1,6 @@ import {inject, customElement, bindable, containerless, TaskQueue} from 'aurelia-framework'; import {BindingEngine} from 'aurelia-binding'; +import {EventAggregator} from 'aurelia-event-aggregator'; import $ from 'jquery'; import 'npm:gsap@1.18.0/src/minified/TweenMax.min.js'; import d3 from 'd3'; @@ -9,13 +10,14 @@ import {fillColor} from './node-style'; @customElement('node') @containerless() -@inject(Element, BindingEngine, GraphTextService, TaskQueue, NodeCalculator) +@inject(Element, BindingEngine, EventAggregator, GraphTextService, TaskQueue, NodeCalculator) export class Node { @bindable data; - constructor(element, bindingEngine, graphTextService, taskQueue, nodeCalculator) { + constructor(element, bindingEngine, eventAggregator, graphTextService, taskQueue, nodeCalculator) { this.element = element; this.bindingEngine = bindingEngine; + this.eventAggregator = eventAggregator; this.graphTextService = graphTextService; this.taskQueue = taskQueue; this.nodeCalculator = nodeCalculator; @@ -81,6 +83,9 @@ export class Node { this.yChangeSub = this.bindingEngine.propertyObserver(this.displayNode, 'y').subscribe((newValue, oldValue) => { this.animateY(this.$node, oldValue, newValue); }); + + this.$node.on('mouseenter.node', this.handleMouseIn.bind(this)); + this.$node.on('mouseleave.node', this.handleMouseOut.bind(this)); } animateX(selector, fromPos, toPos) { @@ -97,6 +102,14 @@ export class Node { ); } + handleMouseIn() { + this.eventAggregator.publish('node.hover.in', this.displayNode); + } + + handleMouseOut() { + this.eventAggregator.publish('node.hover.out', this.displayNode); + } + /** * Toggle node selected status */ @@ -131,6 +144,8 @@ export class Node { unregisterEvents() { this.xChangeSub.dispose(); this.yChangeSub.dispose(); + this.$node.off('mouseenter.node'); + this.$node.off('mouseleave.node'); } } diff --git a/styles/edge.css b/styles/edge.css index 12e37f7..3baefc0 100644 --- a/styles/edge.css +++ b/styles/edge.css @@ -1,3 +1,14 @@ +/* FIXME https://github.com/CANVE/canve-viz/issues/55 */ +/* Animate edges but this breaks dash styling to indicate edgeKind */ .edge { - stroke-opacity: .6 + stroke-opacity: .6; + /*stroke-dasharray: 1000; + stroke-dashoffset: 1000; + animation: dash 5s linear forwards;*/ +} + +@keyframes dash { + to { + stroke-dashoffset: 0; + } } diff --git a/test/unit/edge/edge-spec.js b/test/unit/edge/edge-spec.js new file mode 100644 index 0000000..b2c42f9 --- /dev/null +++ b/test/unit/edge/edge-spec.js @@ -0,0 +1,87 @@ +import {Edge} from '../../../src/edge/edge'; + +describe('Edge', function() { + const edgeStyleColor = 'grey'; + let edge, mockElement, mockEventAggregator, mockEdgeStyle; + + beforeEach( () => { + mockElement = {}; + mockEventAggregator = {}; + mockEdgeStyle = { + strokeColor: function() { return edgeStyleColor; } + }; + edge = new Edge(mockElement, mockEventAggregator, mockEdgeStyle); + }); + + describe('edgeColor', () => { + + it('Returns highlight color for source', () => { + // Given + let displayEdge = { edgeKind: 'extends', highlightSource: true }; + + // When + edge.dataChanged(displayEdge); + + // Then + expect(edge.edgeColor).toEqual('orange'); + }); + + it('Returns highlight color for target', () => { + // Given + let displayEdge = { edgeKind: 'extends', highlightTarget: true }; + + // When + edge.dataChanged(displayEdge); + + // Then + expect(edge.edgeColor).toEqual('rgb(60, 234, 245)'); + }); + + it('Returns color based on edge style', () => { + // Given + spyOn(mockEdgeStyle, 'strokeColor').and.callThrough(); + let displayEdge = { edgeKind: 'extends'}; + + // When + edge.dataChanged(displayEdge); + + // Then + expect(edge.edgeColor).toEqual(edgeStyleColor); + expect(mockEdgeStyle.strokeColor).toHaveBeenCalled(); + }); + + }); + + describe('highlightEdges', () => { + + it('Sets edge to be highlighted as source', () => { + // Given + let node = { id: '111'}; + let displayEdge = { source: {id: '111'}, target: {id: '222'}}; + + // When + edge.dataChanged(displayEdge); + edge.highlightEdges(node); + + // Then + expect(edge.displayEdge.highlightSource).toBe(true); + expect(edge.displayEdge.highlightTarget).toBeUndefined(); + }); + + it('Sets edge to be highlighted as target', () => { + // Given + let node = { id: '222'}; + let displayEdge = { source: {id: '111'}, target: {id: '222'}}; + + // When + edge.dataChanged(displayEdge); + edge.highlightEdges(node); + + // Then + expect(edge.displayEdge.highlightTarget).toBe(true); + expect(edge.displayEdge.highlightSource).toBeUndefined(); + }); + + }); + +});