Skip to content

Commit 35fbed9

Browse files
authored
Merge pull request #1 from futuredapp/smsticket-edge-hover-fix
Add edge hover and label fixes
2 parents a97bb47 + 01fa470 commit 35fbed9

5 files changed

Lines changed: 173 additions & 6 deletions

File tree

visualization/app/codeCharta/ui/codeMap/arrow/codeMap.arrow.service.spec.ts

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { appReducers, setStateMiddleware } from "../../../state/store/state.mana
2525
import { clone } from "../../../util/clone"
2626
import { setShowOutgoingEdges } from "../../../state/store/appSettings/showEdges/outgoing/showOutgoingEdges.actions"
2727
import { setShowIncomingEdges } from "../../../state/store/appSettings/showEdges/incoming/showIncomingEdges.actions"
28+
import { setEdgeMetric } from "../../../state/store/dynamicSettings/edgeMetric/edgeMetric.actions"
2829

2930
describe("CodeMapArrowService", () => {
3031
let codeMapArrowService: CodeMapArrowService
@@ -143,7 +144,8 @@ describe("CodeMapArrowService", () => {
143144

144145
store.dispatch(setShowOutgoingEdges({ value: true }))
145146
store.dispatch(setShowIncomingEdges({ value: false }))
146-
store.dispatch(setEdges({ value: [{ fromNodeName: outgoingNode.path, toNodeName: incomingNode.path, attributes: {} }] }))
147+
store.dispatch(setEdgeMetric({ value: "selectedEdgeMetric" }))
148+
store.dispatch(setEdges({ value: [{ fromNodeName: outgoingNode.path, toNodeName: incomingNode.path, attributes: { selectedEdgeMetric: 1 } }] }))
147149

148150
codeMapArrowService["buildPairingEdges"](nodesMap)
149151

@@ -164,13 +166,62 @@ describe("CodeMapArrowService", () => {
164166

165167
store.dispatch(setShowOutgoingEdges({ value: false }))
166168
store.dispatch(setShowIncomingEdges({ value: true }))
167-
store.dispatch(setEdges({ value: [{ fromNodeName: outgoingNode.path, toNodeName: incomingNode.path, attributes: {} }] }))
169+
store.dispatch(setEdgeMetric({ value: "selectedEdgeMetric" }))
170+
store.dispatch(setEdges({ value: [{ fromNodeName: outgoingNode.path, toNodeName: incomingNode.path, attributes: { selectedEdgeMetric: 1 } }] }))
168171

169172
codeMapArrowService["buildPairingEdges"](nodesMap)
170173

171174
expect(codeMapArrowService.addArrow).toHaveBeenCalledTimes(1)
172175
expect(codeMapArrowService.addArrow).toHaveBeenCalledWith(outgoingNode, incomingNode, false)
173176
})
177+
178+
it("should only add hover edges for the selected edge metric", () => {
179+
withMockedThreeSceneService()
180+
codeMapArrowService.addArrow = jest.fn()
181+
182+
const outgoingNode: Node = OUTGOING_NODE
183+
const incomingNode: Node = INCOMING_NODE
184+
const differentNode: Node = DIFFERENT_NODE
185+
const hoveredNodes = new Map<string, Node>()
186+
hoveredNodes.set(outgoingNode.path, outgoingNode)
187+
const nodesMap = new Map<string, Node>()
188+
nodesMap.set(outgoingNode.path, outgoingNode)
189+
nodesMap.set(incomingNode.path, incomingNode)
190+
nodesMap.set(differentNode.path, differentNode)
191+
codeMapArrowService["map"] = nodesMap
192+
193+
store.dispatch(setShowOutgoingEdges({ value: true }))
194+
store.dispatch(setShowIncomingEdges({ value: false }))
195+
store.dispatch(setEdgeMetric({ value: "dependencies" }))
196+
store.dispatch(
197+
setEdges({
198+
value: [
199+
{ fromNodeName: outgoingNode.path, toNodeName: incomingNode.path, attributes: { dependencies: 1 } },
200+
{ fromNodeName: outgoingNode.path, toNodeName: differentNode.path, attributes: { temporal_coupling: 1 } }
201+
]
202+
})
203+
)
204+
205+
codeMapArrowService["buildPairingEdges"](hoveredNodes)
206+
207+
expect(codeMapArrowService.addArrow).toHaveBeenCalledTimes(1)
208+
expect(codeMapArrowService.addArrow).toHaveBeenCalledWith(outgoingNode, incomingNode, true)
209+
})
210+
211+
it("should add hover arrows when the selected height metric value is zero", () => {
212+
withMockedThreeSceneService()
213+
codeMapArrowService["highlightBuilding"] = jest.fn()
214+
codeMapArrowService["setCurveColor"] = jest.fn()
215+
216+
const outgoingNode: Node = { ...OUTGOING_NODE, attributes: { ...OUTGOING_NODE.attributes, mcc: 0 } }
217+
const incomingNode: Node = { ...INCOMING_NODE, attributes: { ...INCOMING_NODE.attributes, mcc: 0 } }
218+
219+
store.dispatch(setHeightMetric({ value: "mcc" }))
220+
221+
codeMapArrowService.addArrow(outgoingNode, incomingNode, true)
222+
223+
expect(codeMapArrowService["setCurveColor"]).toHaveBeenCalledTimes(1)
224+
})
174225
})
175226

176227
describe("SelectionMethods", () => {

visualization/app/codeCharta/ui/codeMap/arrow/codeMap.arrow.service.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,10 @@ export class CodeMapArrowService implements OnDestroy {
103103
const { appSettings, dynamicSettings } = this.state.getValue()
104104
const curveScale = 100 * appSettings.edgeHeight
105105

106-
if (arrowTargetNode.attributes?.[dynamicSettings.heightMetric] && arrowOriginNode.attributes?.[dynamicSettings.heightMetric]) {
106+
if (
107+
arrowTargetNode.attributes?.[dynamicSettings.heightMetric] !== undefined &&
108+
arrowOriginNode.attributes?.[dynamicSettings.heightMetric] !== undefined
109+
) {
107110
const curve = this.createCurve(arrowOriginNode, arrowTargetNode, curveScale)
108111
const color = ColorConverter.getNumber(appSettings.mapColors[buildingIsOriginNode ? "outgoingEdge" : "incomingEdge"])
109112
this.highlightBuilding(buildingIsOriginNode ? arrowTargetNode : arrowOriginNode)
@@ -171,8 +174,16 @@ export class CodeMapArrowService implements OnDestroy {
171174
return
172175
}
173176
const { edges } = this.state.getValue().fileSettings
177+
const { edgeMetric } = this.state.getValue().dynamicSettings
178+
if (edgeMetric === null) {
179+
return
180+
}
174181

175182
for (const edge of edges) {
183+
if (edge.attributes[edgeMetric] === undefined) {
184+
continue
185+
}
186+
176187
const originNode = this.map.get(edge.fromNodeName)
177188
// TODO: Maps should only have valid edges. If that's not the case, the
178189
// internal decoration is likely faulty. Check if only test data is not

visualization/app/codeCharta/ui/codeMap/codeMap.render.service.spec.ts

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,8 @@ describe("codeMapRenderService", () => {
111111
}),
112112
dispose: jest.fn(),
113113
forceRerender: jest.fn(),
114-
setMapMesh: jest.fn()
114+
setMapMesh: jest.fn(),
115+
getHighlightedBuildings: jest.fn().mockReturnValue([])
115116
})()
116117
}
117118

@@ -408,6 +409,53 @@ describe("codeMapRenderService", () => {
408409
})
409410
})
410411

412+
describe("onHighlightChanged", () => {
413+
it("should label every highlighted leaf while hovering", () => {
414+
const currentState = state.getValue()
415+
jest.spyOn(state, "getValue").mockReturnValue({
416+
...currentState,
417+
appStatus: { ...currentState.appStatus, hoveredNodeId: TEST_NODE_ROOT.id }
418+
})
419+
threeSceneService.getHighlightedBuildings = jest.fn().mockReturnValue([
420+
{ node: TEST_NODE_ROOT },
421+
{ node: TEST_NODE_LEAF },
422+
{ node: TEST_NODE_LEAF },
423+
{ node: { ...INCOMING_NODE, visible: false } }
424+
])
425+
426+
codeMapRenderService["onHighlightChanged"]()
427+
428+
expect(labelSettingsFacade.clearLabels).toHaveBeenCalled()
429+
expect(labelSettingsFacade.addLeafLabel).toHaveBeenCalledTimes(2)
430+
expect(labelSettingsFacade.addLeafLabel).toHaveBeenCalledWith(TEST_NODE_ROOT, 2, true)
431+
expect(labelSettingsFacade.addLeafLabel).toHaveBeenCalledWith(TEST_NODE_LEAF, 2, true)
432+
})
433+
434+
it("should restore configured labels when there is no active hover", () => {
435+
codeMapRenderService["unflattenedNodes"] = TEST_NODES
436+
codeMapRenderService["setLabels"] = jest.fn()
437+
438+
codeMapRenderService["onHighlightChanged"]()
439+
440+
expect(codeMapRenderService["setLabels"]).toHaveBeenCalledWith(TEST_NODES)
441+
})
442+
443+
it("should not show hover labels when node names are disabled", () => {
444+
const currentState = state.getValue()
445+
jest.spyOn(state, "getValue").mockReturnValue({
446+
...currentState,
447+
appSettings: { ...currentState.appSettings, showMetricLabelNodeName: false },
448+
appStatus: { ...currentState.appStatus, hoveredNodeId: TEST_NODE_ROOT.id }
449+
})
450+
threeSceneService.getHighlightedBuildings = jest.fn().mockReturnValue([{ node: TEST_NODE_ROOT }])
451+
452+
codeMapRenderService["onHighlightChanged"]()
453+
454+
expect(labelSettingsFacade.clearLabels).toHaveBeenCalled()
455+
expect(labelSettingsFacade.addLeafLabel).not.toHaveBeenCalled()
456+
})
457+
})
458+
411459
describe("setArrows", () => {
412460
let sortedNodes: Node[]
413461

visualization/app/codeCharta/ui/codeMap/codeMap.render.service.ts

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { metricDataSelector } from "../../state/selectors/accumulatedData/metric
1515
import { blacklistMatcherSelector } from "../../state/store/fileSettings/blacklist/blacklistMatcher.selector"
1616
import { State, Store } from "@ngrx/store"
1717
import { setColorLabels } from "../../state/store/appSettings/colorLabels/colorLabels.actions"
18+
import { hoveredNodeIdSelector } from "../../state/store/appStatus/hoveredNodeId/hoveredNodeId.selector"
1819

1920
export interface ColorCategoryCounts {
2021
positive: number
@@ -46,6 +47,7 @@ export class CodeMapRenderService implements OnDestroy {
4647
private codeMapMouseEventService: CodeMapMouseEventService
4748
) {
4849
this.subscription = this.store.select(isLoadingFileSelector).pipe(tap(this.onIsLoadingFileChanged)).subscribe()
50+
this.threeSceneService.subscribe("onHighlightChanged", this.onHighlightChanged)
4951
}
5052

5153
ngOnDestroy(): void {
@@ -175,12 +177,55 @@ export class CodeMapRenderService implements OnDestroy {
175177
}
176178
}
177179

178-
private setBuildingLabel(nodes: Node[], highestNodeInSet: number) {
180+
private setBuildingLabel(nodes: Node[], highestNodeInSet: number, enforceLabel = false) {
179181
for (const node of nodes) {
180-
this.labelSettingsFacade.addLeafLabel(node, highestNodeInSet)
182+
this.labelSettingsFacade.addLeafLabel(node, highestNodeInSet, enforceLabel)
181183
}
182184
}
183185

186+
private onHighlightChanged = () => {
187+
if (hoveredNodeIdSelector(this.state.getValue()) === null) {
188+
this.setLabels(this.unflattenedNodes)
189+
return
190+
}
191+
192+
if (!this.state.getValue().appSettings.showMetricLabelNodeName) {
193+
this.labelSettingsFacade.clearLabels()
194+
return
195+
}
196+
197+
const highlightedNodes = this.getHighlightedLeafNodes()
198+
this.labelSettingsFacade.clearLabels()
199+
200+
if (highlightedNodes.length === 0) {
201+
return
202+
}
203+
204+
const highestNodeInSet = Math.max(...highlightedNodes.map(node => node.height))
205+
this.setBuildingLabel(
206+
highlightedNodes.sort((a, b) => b.height - a.height),
207+
highestNodeInSet,
208+
true
209+
)
210+
}
211+
212+
private getHighlightedLeafNodes() {
213+
const highlightedNodes: Node[] = []
214+
const seenNodeIds = new Set<number>()
215+
216+
for (const building of this.threeSceneService.getHighlightedBuildings()) {
217+
const { node } = building
218+
if (!node?.isLeaf || node.flat || !node.visible || seenNodeIds.has(node.id)) {
219+
continue
220+
}
221+
222+
seenNodeIds.add(node.id)
223+
highlightedNodes.push(node)
224+
}
225+
226+
return highlightedNodes
227+
}
228+
184229
private setLabels(sortedNodes: Node[]) {
185230
this.labelSettingsFacade.clearLabels()
186231

visualization/app/codeCharta/ui/codeMap/threeViewer/threeSceneService.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { FileExtensionCalculator, NO_EXTENSION } from "../../fileExtensionBar/se
1919
type BuildingSelectedEvents = {
2020
onBuildingSelected: (data: { building: CodeMapBuilding }) => void
2121
onBuildingDeselected: () => void
22+
onHighlightChanged: () => void
2223
}
2324

2425
@Injectable({ providedIn: "root" })
@@ -154,6 +155,7 @@ export class ThreeSceneService implements OnDestroy {
154155
const constantHighlightedNodes = new Set<number>([...this.constantHighlight.values()].map(({ node }) => node.id))
155156
this.highlightMaterial(materials, constantHighlightedNodes)
156157
}
158+
this.eventEmitter.emit("onHighlightChanged")
157159
this.threeRendererService.render()
158160
}
159161

@@ -239,6 +241,7 @@ export class ThreeSceneService implements OnDestroy {
239241
if (materials) {
240242
this.resetMaterial(materials)
241243
}
244+
this.eventEmitter.emit("onHighlightChanged")
242245
}
243246
}
244247

@@ -349,6 +352,15 @@ export class ThreeSceneService implements OnDestroy {
349352
return this.primaryHighlightedBuilding
350353
}
351354

355+
getHighlightedBuildings() {
356+
if (!this.mapMesh) {
357+
return []
358+
}
359+
360+
const { buildings } = this.mapMesh.getMeshDescription()
361+
return [...this.highlightedBuildingIds].map(id => buildings[id]).filter((building): building is CodeMapBuilding => Boolean(building))
362+
}
363+
352364
dispose() {
353365
this.mapMesh?.dispose()
354366
}

0 commit comments

Comments
 (0)