From 16173a05e41080a2e512e5f00852ba6a8ac7e19c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrik=20Potoc=CC=8Cek?= Date: Sat, 9 May 2026 14:06:23 +0200 Subject: [PATCH] Add configurable floor label depth --- .../app/codeCharta/codeCharta.model.ts | 1 + .../scenarios/model/scenario.model.ts | 1 + .../services/scenarioApplier.service.spec.ts | 2 + .../services/scenarioApplier.service.ts | 1 + .../services/scenarios.service.spec.ts | 1 + .../scenarios/services/scenarios.service.ts | 1 + .../loadInitialFile.service.ts | 6 +- .../selectorsTriggeringAutoFit.ts | 2 + .../actionsRequiringRerender.ts | 2 + .../store/appSettings/appSettings.actions.ts | 2 + .../store/appSettings/appSettings.reducer.ts | 3 + .../floorLabelDepth.actions.ts | 3 + .../floorLabelDepth.reducer.spec.ts | 8 +++ .../floorLabelDepth.reducer.ts | 6 ++ .../floorLabelDepth.selector.ts | 4 ++ .../floorLabels/floorLabelDrawer.spec.ts | 47 ++++++++++++-- .../floorLabels/floorLabelDrawer.ts | 63 ++++++++++++++++--- .../floorLabels/floorLabelHelper.spec.ts | 21 +++++-- .../floorLabels/floorLabelHelper.ts | 5 +- .../codeMap/threeViewer/threeSceneService.ts | 3 +- .../areaSettingsPanel.component.html | 12 +++- .../areaSettingsPanel.component.spec.ts | 6 +- .../areaSettingsPanel.component.ts | 7 +++ .../treeMapLayout/treeMapGenerator.spec.ts | 61 +++++++++++++++++- .../treeMapLayout/treeMapGenerator.ts | 30 ++++++--- .../app/codeCharta/util/dataMocks.ts | 2 + 26 files changed, 263 insertions(+), 37 deletions(-) create mode 100644 visualization/app/codeCharta/state/store/appSettings/floorLabelDepth/floorLabelDepth.actions.ts create mode 100644 visualization/app/codeCharta/state/store/appSettings/floorLabelDepth/floorLabelDepth.reducer.spec.ts create mode 100644 visualization/app/codeCharta/state/store/appSettings/floorLabelDepth/floorLabelDepth.reducer.ts create mode 100644 visualization/app/codeCharta/state/store/appSettings/floorLabelDepth/floorLabelDepth.selector.ts diff --git a/visualization/app/codeCharta/codeCharta.model.ts b/visualization/app/codeCharta/codeCharta.model.ts index f2254591cb..703018543a 100644 --- a/visualization/app/codeCharta/codeCharta.model.ts +++ b/visualization/app/codeCharta/codeCharta.model.ts @@ -173,6 +173,7 @@ export interface AppSettings { groupLabelCollisions: boolean isColorMetricLinkedToHeightMetric: boolean enableFloorLabels: boolean + floorLabelDepth: number } export interface MapColors { diff --git a/visualization/app/codeCharta/features/scenarios/model/scenario.model.ts b/visualization/app/codeCharta/features/scenarios/model/scenario.model.ts index 0eb5d91def..bc955a8025 100644 --- a/visualization/app/codeCharta/features/scenarios/model/scenario.model.ts +++ b/visualization/app/codeCharta/features/scenarios/model/scenario.model.ts @@ -37,6 +37,7 @@ export interface LabelsAndFoldersSection { readonly showMetricLabelNameValue: boolean readonly showMetricLabelNodeName: boolean readonly enableFloorLabels: boolean + readonly floorLabelDepth?: number readonly colorLabels: ColorLabelOptions readonly labelMode: LabelMode readonly groupLabelCollisions: boolean diff --git a/visualization/app/codeCharta/features/scenarios/services/scenarioApplier.service.spec.ts b/visualization/app/codeCharta/features/scenarios/services/scenarioApplier.service.spec.ts index a513af8c53..0b190a4326 100644 --- a/visualization/app/codeCharta/features/scenarios/services/scenarioApplier.service.spec.ts +++ b/visualization/app/codeCharta/features/scenarios/services/scenarioApplier.service.spec.ts @@ -53,6 +53,7 @@ const testSections: ScenarioSections = { showMetricLabelNameValue: true, showMetricLabelNodeName: false, enableFloorLabels: true, + floorLabelDepth: 4, colorLabels: { positive: true, negative: false, neutral: false }, labelMode: LabelMode.Color, groupLabelCollisions: true, @@ -157,6 +158,7 @@ describe("ScenarioApplierService", () => { expect(patches[2].fileSettings?.blacklist).toEqual(testSections.filters.blacklist) expect(patches[2].dynamicSettings?.focusedNodePath).toEqual(["/root/src"]) expect(patches[2].appSettings?.amountOfTopLabels).toBe(5) + expect(patches[2].appSettings?.floorLabelDepth).toBe(4) expect(patches[2].appSettings?.labelMode).toBe(LabelMode.Color) expect(patches[2].appSettings?.groupLabelCollisions).toBe(true) expect(patches[2].fileSettings?.markedPackages).toEqual(testSections.labelsAndFolders.markedPackages) diff --git a/visualization/app/codeCharta/features/scenarios/services/scenarioApplier.service.ts b/visualization/app/codeCharta/features/scenarios/services/scenarioApplier.service.ts index 9f999411e3..446c078c1a 100644 --- a/visualization/app/codeCharta/features/scenarios/services/scenarioApplier.service.ts +++ b/visualization/app/codeCharta/features/scenarios/services/scenarioApplier.service.ts @@ -197,6 +197,7 @@ export class ScenarioApplierService { showMetricLabelNameValue: labelsAndFolders.showMetricLabelNameValue, showMetricLabelNodeName: labelsAndFolders.showMetricLabelNodeName, enableFloorLabels: labelsAndFolders.enableFloorLabels, + ...(labelsAndFolders.floorLabelDepth === undefined ? {} : { floorLabelDepth: labelsAndFolders.floorLabelDepth }), colorLabels: labelsAndFolders.colorLabels, labelMode: labelsAndFolders.labelMode, groupLabelCollisions: labelsAndFolders.groupLabelCollisions diff --git a/visualization/app/codeCharta/features/scenarios/services/scenarios.service.spec.ts b/visualization/app/codeCharta/features/scenarios/services/scenarios.service.spec.ts index c9cdafc0e7..10a0273b16 100644 --- a/visualization/app/codeCharta/features/scenarios/services/scenarios.service.spec.ts +++ b/visualization/app/codeCharta/features/scenarios/services/scenarios.service.spec.ts @@ -168,6 +168,7 @@ describe("ScenariosService", () => { // Assert expect(sections.labelsAndFolders.amountOfTopLabels).toBe(defaultState.appSettings.amountOfTopLabels) expect(sections.labelsAndFolders.labelSize).toBe(defaultState.appSettings.labelSize) + expect(sections.labelsAndFolders.floorLabelDepth).toBe(defaultState.appSettings.floorLabelDepth) expect(sections.labelsAndFolders.labelMode).toBe(defaultState.appSettings.labelMode) expect(sections.labelsAndFolders.groupLabelCollisions).toBe(defaultState.appSettings.groupLabelCollisions) expect(sections.labelsAndFolders.markedPackages).toEqual(defaultState.fileSettings.markedPackages) diff --git a/visualization/app/codeCharta/features/scenarios/services/scenarios.service.ts b/visualization/app/codeCharta/features/scenarios/services/scenarios.service.ts index cccf276e63..9e8474c3e0 100644 --- a/visualization/app/codeCharta/features/scenarios/services/scenarios.service.ts +++ b/visualization/app/codeCharta/features/scenarios/services/scenarios.service.ts @@ -189,6 +189,7 @@ export class ScenariosService { showMetricLabelNameValue: state.appSettings.showMetricLabelNameValue, showMetricLabelNodeName: state.appSettings.showMetricLabelNodeName, enableFloorLabels: state.appSettings.enableFloorLabels, + floorLabelDepth: state.appSettings.floorLabelDepth, colorLabels: { ...state.appSettings.colorLabels }, labelMode: state.appSettings.labelMode, groupLabelCollisions: state.appSettings.groupLabelCollisions, diff --git a/visualization/app/codeCharta/services/loadInitialFile/loadInitialFile.service.ts b/visualization/app/codeCharta/services/loadInitialFile/loadInitialFile.service.ts index ba5d8bade0..df56397ec0 100644 --- a/visualization/app/codeCharta/services/loadInitialFile/loadInitialFile.service.ts +++ b/visualization/app/codeCharta/services/loadInitialFile/loadInitialFile.service.ts @@ -60,6 +60,7 @@ import { setScreenshotToClipboardEnabled } from "../../state/store/appSettings/e import { setColorLabels } from "../../state/store/appSettings/colorLabels/colorLabels.actions" import { setIsColorMetricLinkedToHeightMetricAction } from "../../state/store/appSettings/isHeightAndColorMetricLinked/isColorMetricLinkedToHeightMetric.actions" import { setEnableFloorLabels } from "../../state/store/appSettings/enableFloorLabels/enableFloorLabels.actions" +import { setFloorLabelDepth } from "../../state/store/appSettings/floorLabelDepth/floorLabelDepth.actions" import { setLabelMode } from "../../state/store/appSettings/labelMode/labelMode.actions" import { setGroupLabelCollisions } from "../../state/store/appSettings/groupLabelCollisions/groupLabelCollisions.actions" @@ -237,7 +238,7 @@ export class LoadInitialFileService { return missingDynamicSettings } - private static readonly optionalAppSettingsKeys = new Set(["labelMode", "groupLabelCollisions", "labelSize"]) + private static readonly optionalAppSettingsKeys = new Set(["labelMode", "groupLabelCollisions", "labelSize", "floorLabelDepth"]) private applyAppSettings(savedAppSettings: AppSettings) { const currentAppSettings = (this.state.getValue() as CcState).appSettings @@ -410,6 +411,9 @@ export class LoadInitialFileService { case "enableFloorLabels": this.store.dispatch(setEnableFloorLabels({ value })) break + case "floorLabelDepth": + this.store.dispatch(setFloorLabelDepth({ value })) + break case "labelMode": this.store.dispatch(setLabelMode({ value })) break diff --git a/visualization/app/codeCharta/state/effects/autoFitCodeMapChange/selectorsTriggeringAutoFit.ts b/visualization/app/codeCharta/state/effects/autoFitCodeMapChange/selectorsTriggeringAutoFit.ts index 4412a029ca..f99e61f43b 100644 --- a/visualization/app/codeCharta/state/effects/autoFitCodeMapChange/selectorsTriggeringAutoFit.ts +++ b/visualization/app/codeCharta/state/effects/autoFitCodeMapChange/selectorsTriggeringAutoFit.ts @@ -5,6 +5,7 @@ import { invertAreaSelector } from "../../store/appSettings/invertArea/invertAre import { marginSelector } from "../../store/dynamicSettings/margin/margin.selector" import { DefaultProjectorFn, MemoizedSelector } from "@ngrx/store" import { enableFloorLabelsSelector } from "../../store/appSettings/enableFloorLabels/enableFloorLabels.selector" +import { floorLabelDepthSelector } from "../../store/appSettings/floorLabelDepth/floorLabelDepth.selector" import { areaMetricSelector } from "../../store/dynamicSettings/areaMetric/areaMetric.selector" import { isDeltaStateSelector } from "../../selectors/isDeltaState.selector" @@ -15,6 +16,7 @@ export const selectorsTriggeringAutoFit: MemoizedSelector()) diff --git a/visualization/app/codeCharta/state/store/appSettings/floorLabelDepth/floorLabelDepth.reducer.spec.ts b/visualization/app/codeCharta/state/store/appSettings/floorLabelDepth/floorLabelDepth.reducer.spec.ts new file mode 100644 index 0000000000..fddc53ba4e --- /dev/null +++ b/visualization/app/codeCharta/state/store/appSettings/floorLabelDepth/floorLabelDepth.reducer.spec.ts @@ -0,0 +1,8 @@ +import { setFloorLabelDepth } from "./floorLabelDepth.actions" +import { floorLabelDepth } from "./floorLabelDepth.reducer" + +describe("floorLabelDepth", () => { + it("should set new floor label depth", () => { + expect(floorLabelDepth(3, setFloorLabelDepth({ value: 4 }))).toEqual(4) + }) +}) diff --git a/visualization/app/codeCharta/state/store/appSettings/floorLabelDepth/floorLabelDepth.reducer.ts b/visualization/app/codeCharta/state/store/appSettings/floorLabelDepth/floorLabelDepth.reducer.ts new file mode 100644 index 0000000000..63d5067e7e --- /dev/null +++ b/visualization/app/codeCharta/state/store/appSettings/floorLabelDepth/floorLabelDepth.reducer.ts @@ -0,0 +1,6 @@ +import { createReducer, on } from "@ngrx/store" +import { setState } from "../../util/setState.reducer.factory" +import { setFloorLabelDepth } from "./floorLabelDepth.actions" + +export const defaultFloorLabelDepth = 3 +export const floorLabelDepth = createReducer(defaultFloorLabelDepth, on(setFloorLabelDepth, setState(defaultFloorLabelDepth))) diff --git a/visualization/app/codeCharta/state/store/appSettings/floorLabelDepth/floorLabelDepth.selector.ts b/visualization/app/codeCharta/state/store/appSettings/floorLabelDepth/floorLabelDepth.selector.ts new file mode 100644 index 0000000000..51f379c86b --- /dev/null +++ b/visualization/app/codeCharta/state/store/appSettings/floorLabelDepth/floorLabelDepth.selector.ts @@ -0,0 +1,4 @@ +import { createSelector } from "@ngrx/store" +import { appSettingsSelector } from "../appSettings.selector" + +export const floorLabelDepthSelector = createSelector(appSettingsSelector, appSettings => appSettings.floorLabelDepth) diff --git a/visualization/app/codeCharta/ui/codeMap/threeViewer/floorLabels/floorLabelDrawer.spec.ts b/visualization/app/codeCharta/ui/codeMap/threeViewer/floorLabels/floorLabelDrawer.spec.ts index 1e1be53791..e83eb93c8d 100644 --- a/visualization/app/codeCharta/ui/codeMap/threeViewer/floorLabels/floorLabelDrawer.spec.ts +++ b/visualization/app/codeCharta/ui/codeMap/threeViewer/floorLabels/floorLabelDrawer.spec.ts @@ -71,6 +71,7 @@ describe("FloorLabelDrawer", () => { const mapSize = 500 const scaling: Vector3 = { x: 1, y: 1, z: 1 } as Vector3 + const floorLabelDepth = 3 describe("draw", () => { it("should draw simple and shortened labels on three levels", () => { @@ -87,7 +88,7 @@ describe("FloorLabelDrawer", () => { const canvasContextMock = createCanvasMock() - const floorLabelDrawer = new FloorLabelDrawer(nodes, rootNode, mapSize, scaling, false) + const floorLabelDrawer = new FloorLabelDrawer(nodes, rootNode, mapSize, scaling, floorLabelDepth, false) const floorLabelPlanes = floorLabelDrawer.draw() expect(canvasContextMock.fillText).toHaveBeenCalledTimes(5) @@ -112,7 +113,7 @@ describe("FloorLabelDrawer", () => { createFakeNode("text_to_be_shortened_to_fit_onto_the_floor", 50, 50, false, 2) ] - const floorLabelDrawer = new FloorLabelDrawer(nodes, rootNode, mapSize, scaling, true) + const floorLabelDrawer = new FloorLabelDrawer(nodes, rootNode, mapSize, scaling, floorLabelDepth, true) const floorLabelPlanes = floorLabelDrawer.draw() expect(floorLabelDrawer.folderGeometryHeight).toBe(68) @@ -135,13 +136,49 @@ describe("FloorLabelDrawer", () => { const canvasContextMock = createCanvasMock() - const floorLabelDrawer = new FloorLabelDrawer(nodes, rootNode, mapSize, scaling, false) + const floorLabelDrawer = new FloorLabelDrawer(nodes, rootNode, mapSize, scaling, floorLabelDepth, false) const floorLabelPlanes = floorLabelDrawer.draw() expect(canvasContextMock.fillText).toHaveBeenCalledTimes(4) expect(floorLabelPlanes.length).toBe(3) }) + it("should draw on configured floor label depth", () => { + initMapCanvas() + + const rootNode = createFakeNode("root", 500, 500, false, 0) + const nodes = [ + rootNode, + createFakeNode("simpleLabelNode1", 400, 400, false, 1), + createFakeNode("simpleLabelNode2", 200, 200, false, 2), + createFakeNode("simpleLabelNode3", 50, 50, false, 3) + ] + + const canvasContextMock = createCanvasMock() + + const floorLabelDrawer = new FloorLabelDrawer(nodes, rootNode, mapSize, scaling, 4, false) + const floorLabelPlanes = floorLabelDrawer.draw() + + expect(canvasContextMock.fillText).toHaveBeenCalledTimes(4) + expect(floorLabelPlanes.length).toBe(4) + }) + + it("should skip configured deep labels when their reserved label lane is too small", () => { + initMapCanvas() + + const rootNode = createFakeNode("root", 500, 500, false, 0) + const nodes = [rootNode, createFakeNode("coveredLabel", 20, 200, false, 3)] + + const canvasContextMock = createCanvasMock() + + const floorLabelDrawer = new FloorLabelDrawer(nodes, rootNode, mapSize, scaling, 4, false) + const floorLabelPlanes = floorLabelDrawer.draw() + + expect(canvasContextMock.fillText).toHaveBeenCalledTimes(1) + expect(canvasContextMock.fillText).toHaveBeenCalledWith("root", expect.any(Number), expect.any(Number)) + expect(floorLabelPlanes.length).toBe(1) + }) + it("should not draw on more levels than needed'", () => { initMapCanvas() @@ -154,7 +191,7 @@ describe("FloorLabelDrawer", () => { const canvasContextMock = createCanvasMock() - const floorLabelDrawer = new FloorLabelDrawer(nodes, rootNode, mapSize, scaling, false) + const floorLabelDrawer = new FloorLabelDrawer(nodes, rootNode, mapSize, scaling, floorLabelDepth, false) const floorLabelPlanes = floorLabelDrawer.draw() expect(canvasContextMock.fillText).toHaveBeenCalledTimes(2) @@ -173,7 +210,7 @@ describe("FloorLabelDrawer", () => { createFakeNode("unlabeledNode", 100, 100, true, 1) ] - const floorLabelDrawer = new FloorLabelDrawer(nodes, rootNode, mapSize, scaling, false) + const floorLabelDrawer = new FloorLabelDrawer(nodes, rootNode, mapSize, scaling, floorLabelDepth, false) const floorLabelPlanes = floorLabelDrawer.draw() const geometryPositions = floorLabelPlanes[0].geometry.attributes.position.array diff --git a/visualization/app/codeCharta/ui/codeMap/threeViewer/floorLabels/floorLabelDrawer.ts b/visualization/app/codeCharta/ui/codeMap/threeViewer/floorLabels/floorLabelDrawer.ts index 6d517b933c..6d634953c3 100644 --- a/visualization/app/codeCharta/ui/codeMap/threeViewer/floorLabels/floorLabelDrawer.ts +++ b/visualization/app/codeCharta/ui/codeMap/threeViewer/floorLabels/floorLabelDrawer.ts @@ -4,6 +4,11 @@ import { Node } from "../../../../codeCharta.model" import { CanvasTexture, BackSide, Mesh, MeshBasicMaterial, PlaneGeometry, RepeatWrapping, Vector3 } from "three" import { FloorLabelHelper } from "./floorLabelHelper" +const DEFAULT_FLOOR_LABEL_DEPTH = 3 +const MAX_LABEL_PADDING_SHARE = 0.25 +const MIN_CONTENT_WIDTH_AFTER_LABEL_PADDING = 10 +const MIN_DEEP_LABEL_FONT_SIZE = 10 + export class FloorLabelDrawer { private floorLabelPlanes: Mesh[] = [] private readonly rootNode: Node @@ -15,8 +20,15 @@ export class FloorLabelDrawer { private floorLabelsPerLevel = new Map() - constructor(nodes: Node[], rootNode: Node, mapSize: number, scaling: Vector3, experimentalFeaturesEnabled: boolean) { - this.collectLabelsPerLevel(nodes) + constructor( + nodes: Node[], + rootNode: Node, + mapSize: number, + scaling: Vector3, + floorLabelDepth: number, + experimentalFeaturesEnabled: boolean + ) { + this.collectLabelsPerLevel(nodes, floorLabelDepth) this.rootNode = rootNode this.mapSize = mapSize this.scaling = scaling @@ -25,9 +37,9 @@ export class FloorLabelDrawer { : 2.01 } - private collectLabelsPerLevel(nodes: Node[]) { + private collectLabelsPerLevel(nodes: Node[], floorLabelDepth: number) { for (const node of nodes) { - if (FloorLabelHelper.isLabelNode(node)) { + if (FloorLabelHelper.isLabelNode(node, floorLabelDepth)) { if (!this.floorLabelsPerLevel.has(node.mapNodeDepth)) { this.floorLabelsPerLevel.set(node.mapNodeDepth, []) } @@ -45,8 +57,10 @@ export class FloorLabelDrawer { for (const [floorLevel, floorNodesPerLevel] of this.floorLabelsPerLevel) { const { textCanvas, context } = FloorLabelDrawer.createLabelPlaneCanvas(scaledMapWidth, scaledMapHeight) - this.writeLabelsOnCanvas(context, floorNodesPerLevel, mapResolutionScaling) - this.drawLevelPlaneGeometry(textCanvas, scaledMapWidth, scaledMapHeight, floorLevel, mapResolutionScaling) + const writtenLabels = this.writeLabelsOnCanvas(context, floorNodesPerLevel, mapResolutionScaling) + if (writtenLabels > 0) { + this.drawLevelPlaneGeometry(textCanvas, scaledMapWidth, scaledMapHeight, floorLevel, mapResolutionScaling) + } } return this.floorLabelPlanes @@ -88,22 +102,37 @@ export class FloorLabelDrawer { private writeLabelsOnCanvas(context: CanvasRenderingContext2D, floorNodesOfCurrentLevel: Node[], mapResolutionScaling: number) { const { width: rootNodeWidth, length: rootNodeHeight } = this.rootNode + let writtenLabels = 0 for (const floorNode of floorNodesOfCurrentLevel) { let fontSize = floorNode.depth === 0 ? Math.max(Math.floor(rootNodeWidth * 0.03), 120) : Math.max(Math.floor(rootNodeWidth * 0.023), 95) fontSize = fontSize * mapResolutionScaling + const maximumFontSize = FloorLabelDrawer.getMaximumFontSize(floorNode, mapResolutionScaling) + + if (maximumFontSize < MIN_DEEP_LABEL_FONT_SIZE * mapResolutionScaling) { + continue + } context.font = `${fontSize}px Arial` - const textToFill = FloorLabelDrawer.getLabelAndSetContextFont(floorNode, context, mapResolutionScaling, fontSize) + const textToFill = FloorLabelDrawer.getLabelAndSetContextFont( + floorNode, + context, + mapResolutionScaling, + fontSize, + maximumFontSize + ) context.fillText( textToFill.labelText, (rootNodeHeight - floorNode.y0 - floorNode.length / 2) * mapResolutionScaling, (floorNode.x0 + floorNode.width) * mapResolutionScaling - textToFill.fontSize / 2 ) + writtenLabels++ } + + return writtenLabels } private drawLevelPlaneGeometry(textCanvas, scaledMapWidth, scaledMapHeight, floorLevel, mapResolutionScaling) { @@ -147,7 +176,8 @@ export class FloorLabelDrawer { labelNode: Node, context: CanvasRenderingContext2D, mapResolutionScaling: number, - fontSize: number + fontSize: number, + maximumFontSize: number ) { const labelText = labelNode.name const floorWidth = labelNode.length * mapResolutionScaling @@ -160,18 +190,31 @@ export class FloorLabelDrawer { // Font will be to small. // So scale text not smaller than 0.5 and shorten it as well fontSize = fontSize * 0.5 - fontSize = Math.floor(Math.min(fontSize, labelNode.width * mapResolutionScaling)) + fontSize = Math.floor(Math.min(fontSize, maximumFontSize)) context.font = `${fontSize}px Arial` return { labelText: FloorLabelDrawer.getFittingLabelText(context, floorWidth, labelText), fontSize } } - fontSize = Math.floor(Math.min(fontSize * fontScaleFactor, labelNode.width * mapResolutionScaling)) + fontSize = Math.floor(Math.min(fontSize * fontScaleFactor, maximumFontSize)) context.font = `${fontSize}px Arial` return { labelText, fontSize } } + private static getMaximumFontSize(labelNode: Node, mapResolutionScaling: number) { + if (labelNode.mapNodeDepth < DEFAULT_FLOOR_LABEL_DEPTH) { + return labelNode.width * mapResolutionScaling + } + + const labelLaneWidth = Math.min( + labelNode.width * MAX_LABEL_PADDING_SHARE, + Math.max(0, labelNode.width - MIN_CONTENT_WIDTH_AFTER_LABEL_PADDING) + ) + + return labelLaneWidth * mapResolutionScaling + } + private static getFontScaleFactor(canvasWidth: number, widthOfText: number) { return widthOfText < canvasWidth ? 1 : canvasWidth / widthOfText } diff --git a/visualization/app/codeCharta/ui/codeMap/threeViewer/floorLabels/floorLabelHelper.spec.ts b/visualization/app/codeCharta/ui/codeMap/threeViewer/floorLabels/floorLabelHelper.spec.ts index 9e080020d2..d672854131 100644 --- a/visualization/app/codeCharta/ui/codeMap/threeViewer/floorLabels/floorLabelHelper.spec.ts +++ b/visualization/app/codeCharta/ui/codeMap/threeViewer/floorLabels/floorLabelHelper.spec.ts @@ -36,6 +36,8 @@ describe("FloorLabelHelper", () => { }) describe("isLabelNode", () => { + const floorLabelDepth = 3 + function createNode(isLeaf: boolean, mapNodeDepth?: number): Node { return { attributes: undefined, @@ -65,24 +67,31 @@ describe("FloorLabelHelper", () => { it("should return true for floor label nodes)", () => { const nodeLevel0 = createNode(false, 0) - expect(FloorLabelHelper.isLabelNode(nodeLevel0)).toBe(true) + expect(FloorLabelHelper.isLabelNode(nodeLevel0, floorLabelDepth)).toBe(true) const nodeLevel1 = createNode(false, 1) - expect(FloorLabelHelper.isLabelNode(nodeLevel1)).toBe(true) + expect(FloorLabelHelper.isLabelNode(nodeLevel1, floorLabelDepth)).toBe(true) const nodeLevel2 = createNode(false, 2) - expect(FloorLabelHelper.isLabelNode(nodeLevel2)).toBe(true) + expect(FloorLabelHelper.isLabelNode(nodeLevel2, floorLabelDepth)).toBe(true) }) it("should return false for other nodes)", () => { const node1 = createNode(true, 0) - expect(FloorLabelHelper.isLabelNode(node1)).toBe(false) + expect(FloorLabelHelper.isLabelNode(node1, floorLabelDepth)).toBe(false) const node2 = createNode(false, 3) - expect(FloorLabelHelper.isLabelNode(node2)).toBe(false) + expect(FloorLabelHelper.isLabelNode(node2, floorLabelDepth)).toBe(false) const node3 = createNode(true) - expect(FloorLabelHelper.isLabelNode(node3)).toBe(false) + expect(FloorLabelHelper.isLabelNode(node3, floorLabelDepth)).toBe(false) + }) + + it("should respect configured floor label depth", () => { + const nodeLevel3 = createNode(false, 3) + + expect(FloorLabelHelper.isLabelNode(nodeLevel3, 3)).toBe(false) + expect(FloorLabelHelper.isLabelNode(nodeLevel3, 4)).toBe(true) }) }) }) diff --git a/visualization/app/codeCharta/ui/codeMap/threeViewer/floorLabels/floorLabelHelper.ts b/visualization/app/codeCharta/ui/codeMap/threeViewer/floorLabels/floorLabelHelper.ts index 1ee6e028fe..4c810d3757 100644 --- a/visualization/app/codeCharta/ui/codeMap/threeViewer/floorLabels/floorLabelHelper.ts +++ b/visualization/app/codeCharta/ui/codeMap/threeViewer/floorLabels/floorLabelHelper.ts @@ -1,7 +1,6 @@ "use strict" import { Node } from "../../../../codeCharta.model" -import { HIERARCHY_LEVELS_WITH_LABLES_UPPER_BOUNDARY } from "../../../../util/algorithm/treeMapLayout/treeMapGenerator" export class FloorLabelHelper { static getMapResolutionScaling(mapWidth: number) { @@ -17,7 +16,7 @@ export class FloorLabelHelper { return Math.min(displayWidth * 4, fullHdPlusWidth * 4) } - static isLabelNode(node: Node) { - return !node.isLeaf && node.mapNodeDepth < HIERARCHY_LEVELS_WITH_LABLES_UPPER_BOUNDARY + static isLabelNode(node: Node, floorLabelDepth: number) { + return !node.isLeaf && node.mapNodeDepth < floorLabelDepth } } diff --git a/visualization/app/codeCharta/ui/codeMap/threeViewer/threeSceneService.ts b/visualization/app/codeCharta/ui/codeMap/threeViewer/threeSceneService.ts index 50e71bd8c1..aa6d4a5b9b 100755 --- a/visualization/app/codeCharta/ui/codeMap/threeViewer/threeSceneService.ts +++ b/visualization/app/codeCharta/ui/codeMap/threeViewer/threeSceneService.ts @@ -96,7 +96,7 @@ export class ThreeSceneService implements OnDestroy { } this.floorLabelPlanes.clear() - const { layoutAlgorithm, enableFloorLabels } = this.state.getValue().appSettings + const { layoutAlgorithm, enableFloorLabels, floorLabelDepth } = this.state.getValue().appSettings if (layoutAlgorithm !== LayoutAlgorithm.SquarifiedTreeMap || !enableFloorLabels) { return } @@ -114,6 +114,7 @@ export class ThreeSceneService implements OnDestroy { rootNode, treeMapSize, scalingVector, + floorLabelDepth, experimentalFeaturesEnabled ) const floorLabels = this.floorLabelDrawer.draw() diff --git a/visualization/app/codeCharta/ui/ribbonBar/areaSettingsPanel/areaSettingsPanel.component.html b/visualization/app/codeCharta/ui/ribbonBar/areaSettingsPanel/areaSettingsPanel.component.html index 36e3aca799..220c3e67e2 100644 --- a/visualization/app/codeCharta/ui/ribbonBar/areaSettingsPanel/areaSettingsPanel.component.html +++ b/visualization/app/codeCharta/ui/ribbonBar/areaSettingsPanel/areaSettingsPanel.component.html @@ -5,13 +5,21 @@ [value]="margin$ | async" [onChange]="applyDebouncedMargin" [min]="1" - [max]="100" + [max]="1000" > Enable Floor Labels + Invert Area diff --git a/visualization/app/codeCharta/ui/ribbonBar/areaSettingsPanel/areaSettingsPanel.component.spec.ts b/visualization/app/codeCharta/ui/ribbonBar/areaSettingsPanel/areaSettingsPanel.component.spec.ts index 17f79b06d0..d7ac3f8d09 100644 --- a/visualization/app/codeCharta/ui/ribbonBar/areaSettingsPanel/areaSettingsPanel.component.spec.ts +++ b/visualization/app/codeCharta/ui/ribbonBar/areaSettingsPanel/areaSettingsPanel.component.spec.ts @@ -15,7 +15,7 @@ describe("AreaSettingsPanelComponent", () => { }) }) - it("should allow for change and resetting of 'margin', 'enable floor label' and 'invert area'", async () => { + it("should allow for change and resetting of 'margin', 'enable floor label', 'floor label depth' and 'invert area'", async () => { const { fixture } = await render(AreaSettingsPanelComponent) const loader = TestbedHarnessEnvironment.loader(fixture) const state = TestBed.inject(State) @@ -23,10 +23,13 @@ describe("AreaSettingsPanelComponent", () => { const marginInput = await loader.getHarness(MatInputHarness.with({ ancestor: 'cc-slider[label="Margin"]' })) await marginInput.setValue(String(initialValues.margin + 1)) + const floorLabelDepthInput = await loader.getHarness(MatInputHarness.with({ ancestor: 'cc-slider[label="Floor Label Depth"]' })) + await floorLabelDepthInput.setValue(String(initialValues.floorLabelDepth + 1)) await userEvent.click(await screen.findByText("Enable Floor Labels")) await userEvent.click(await screen.findByText("Invert Area")) const changedValues = extractRelatedValues(state.getValue()) expect(changedValues.margin).toBe(initialValues.margin + 1) + expect(changedValues.floorLabelDepth).toBe(initialValues.floorLabelDepth + 1) expect(changedValues.enableFloorLabels).toBe(!initialValues.enableFloorLabels) expect(changedValues.invertArea).toBe(!initialValues.invertArea) @@ -39,6 +42,7 @@ describe("AreaSettingsPanelComponent", () => { return { margin: state.dynamicSettings.margin, enableFloorLabels: state.appSettings.enableFloorLabels, + floorLabelDepth: state.appSettings.floorLabelDepth, invertArea: state.appSettings.invertArea } } diff --git a/visualization/app/codeCharta/ui/ribbonBar/areaSettingsPanel/areaSettingsPanel.component.ts b/visualization/app/codeCharta/ui/ribbonBar/areaSettingsPanel/areaSettingsPanel.component.ts index dadff30921..9f36ad5e78 100644 --- a/visualization/app/codeCharta/ui/ribbonBar/areaSettingsPanel/areaSettingsPanel.component.ts +++ b/visualization/app/codeCharta/ui/ribbonBar/areaSettingsPanel/areaSettingsPanel.component.ts @@ -4,6 +4,8 @@ import { setMargin } from "../../../state/store/dynamicSettings/margin/margin.ac import { MatCheckboxChange, MatCheckbox } from "@angular/material/checkbox" import { setEnableFloorLabels } from "../../../state/store/appSettings/enableFloorLabels/enableFloorLabels.actions" import { enableFloorLabelsSelector } from "../../../state/store/appSettings/enableFloorLabels/enableFloorLabels.selector" +import { setFloorLabelDepth } from "../../../state/store/appSettings/floorLabelDepth/floorLabelDepth.actions" +import { floorLabelDepthSelector } from "../../../state/store/appSettings/floorLabelDepth/floorLabelDepth.selector" import { invertAreaSelector } from "../../../state/store/appSettings/invertArea/invertArea.selector" import { debounce } from "../../../util/debounce" import { setInvertArea } from "../../../state/store/appSettings/invertArea/invertArea.actions" @@ -24,6 +26,7 @@ export class AreaSettingsPanelComponent { margin$ = this.store.select(marginSelector) enableFloorLabels$ = this.store.select(enableFloorLabelsSelector) + floorLabelDepth$ = this.store.select(floorLabelDepthSelector) isInvertedArea$ = this.store.select(invertAreaSelector) constructor(private store: Store) {} @@ -32,6 +35,10 @@ export class AreaSettingsPanelComponent { this.store.dispatch(setMargin({ value: margin })) }, AreaSettingsPanelComponent.DEBOUNCE_TIME) + applyDebouncedFloorLabelDepth = debounce((floorLabelDepth: number) => { + this.store.dispatch(setFloorLabelDepth({ value: floorLabelDepth })) + }, AreaSettingsPanelComponent.DEBOUNCE_TIME) + setEnableFloorLabel(event: MatCheckboxChange) { this.store.dispatch(setEnableFloorLabels({ value: event.checked })) } diff --git a/visualization/app/codeCharta/util/algorithm/treeMapLayout/treeMapGenerator.spec.ts b/visualization/app/codeCharta/util/algorithm/treeMapLayout/treeMapGenerator.spec.ts index 0322af2713..e28d4e9e48 100644 --- a/visualization/app/codeCharta/util/algorithm/treeMapLayout/treeMapGenerator.spec.ts +++ b/visualization/app/codeCharta/util/algorithm/treeMapLayout/treeMapGenerator.spec.ts @@ -1,4 +1,4 @@ -import { NodeMetricData, CcState, CodeMapNode, Node, NameDataPair } from "../../../codeCharta.model" +import { NodeMetricData, CcState, CodeMapNode, Node, NameDataPair, NodeType } from "../../../codeCharta.model" import { METRIC_DATA, TEST_FILE_WITH_PATHS, @@ -137,6 +137,65 @@ describe("treeMapGenerator", () => { } expect(nodes).toMatchSnapshot() }) + + it("should keep small buildings visible when deeper floor labels are enabled", () => { + map = { + name: "root", + type: NodeType.FOLDER, + attributes: {}, + path: "/root", + children: [ + { + name: "Large.swift", + type: NodeType.FILE, + attributes: { rloc: 1000, mcc: 1 }, + path: "/root/Large.swift" + }, + { + name: "SmallFeature", + type: NodeType.FOLDER, + attributes: {}, + path: "/root/SmallFeature", + children: [ + { + name: "Presentation", + type: NodeType.FOLDER, + attributes: {}, + path: "/root/SmallFeature/Presentation", + children: [ + { + name: "Views", + type: NodeType.FOLDER, + attributes: {}, + path: "/root/SmallFeature/Presentation/Views", + children: [ + { + name: "TinyView.swift", + type: NodeType.FILE, + attributes: { rloc: 200, mcc: 1 }, + path: "/root/SmallFeature/Presentation/Views/TinyView.swift" + } + ] + } + ] + } + ] + } + ] + } + state.appSettings.floorLabelDepth = 8 + metricData = [ + { name: "rloc", maxValue: 1000, minValue: 1, values: [1, 1000] }, + { name: "mcc", maxValue: 1, minValue: 1, values: [1] } + ] + + const nodes: Node[] = SquarifiedLayoutGenerator.createTreemapNodes(map, state, metricData, isDeltaState) + const tinyView = nodes.find(node => node.path === "/root/SmallFeature/Presentation/Views/TinyView.swift") + + expect(tinyView).toBeDefined() + expect(tinyView!.width).toBeGreaterThan(0) + expect(tinyView!.length).toBeGreaterThan(0) + }) }) describe("CodeMap value calculation", () => { diff --git a/visualization/app/codeCharta/util/algorithm/treeMapLayout/treeMapGenerator.ts b/visualization/app/codeCharta/util/algorithm/treeMapLayout/treeMapGenerator.ts index 532ff01d42..7af76698a7 100644 --- a/visualization/app/codeCharta/util/algorithm/treeMapLayout/treeMapGenerator.ts +++ b/visualization/app/codeCharta/util/algorithm/treeMapLayout/treeMapGenerator.ts @@ -10,7 +10,9 @@ const DEFAULT_PADDING_FLOOR_LABEL_FROM_LEVEL_1 = 120 const DEFAULT_PADDING_FLOOR_LABEL_FROM_LEVEL_2 = 95 const DEFAULT_ROOT_FLOOR_LABEL_SCALING = 0.035 const DEFAULT_SUB_FLOOR_LABEL_SCALING = 0.028 -export const HIERARCHY_LEVELS_WITH_LABLES_UPPER_BOUNDARY = 3 +const DEFAULT_FLOOR_LABEL_DEPTH = 3 +const MAX_LABEL_PADDING_SHARE = 0.25 +const MIN_CONTENT_WIDTH_AFTER_LABEL_PADDING = 10 export function createTreemapNodes(map: CodeMapNode, state: CcState, metricData: NodeMetricData[], isDeltaState: boolean): Node[] { const mapSizeResolutionScaling = getMapResolutionScaleFactor(state.files) @@ -159,7 +161,7 @@ function scaleRoot(root: Node, scaleLength: number, scaleWidth: number) { function getSquarifiedTreeMap(map: CodeMapNode, state: CcState, mapSizeResolutionScaling: number, maxWidth: number): SquarifiedTreeMap { const hierarchyNode = hierarchy(map) const nodesPerSide = getEstimatedNodesPerSide(hierarchyNode) - const { enableFloorLabels, experimentalFeaturesEnabled } = state.appSettings + const { enableFloorLabels, experimentalFeaturesEnabled, floorLabelDepth } = state.appSettings const { margin } = state.dynamicSettings const padding = margin * PADDING_SCALING_FACTOR * mapSizeResolutionScaling @@ -178,11 +180,11 @@ function getSquarifiedTreeMap(map: CodeMapNode, state: CcState, mapSizeResolutio hierarchyNode.eachAfter(node => { // Precalculate the needed paddings for the floor folder labels to be able to expand the default map size // TODO fix estimation, estimation of added label space is inaccurate - if (!isLeaf(node) && enableFloorLabels) { + if (!isLeaf(node) && enableFloorLabels && node.depth < floorLabelDepth) { if (node.depth === 0) { addedLabelSpace += DEFAULT_PADDING_FLOOR_LABEL_FROM_LEVEL_1 } - if (node.depth > 0 && node.depth < HIERARCHY_LEVELS_WITH_LABLES_UPPER_BOUNDARY) { + if (node.depth > 0) { addedLabelSpace += DEFAULT_PADDING_FLOOR_LABEL_FROM_LEVEL_2 } } @@ -209,7 +211,7 @@ function getSquarifiedTreeMap(map: CodeMapNode, state: CcState, mapSizeResolutio // TODO This will not work for FixedFolders // it seems that depth property is missing in that case // so the default padding will be added, which is fine though. - if (rootNode && enableFloorLabels) { + if (rootNode && enableFloorLabels && node.depth < floorLabelDepth) { // Start the labels at level 1 not 0 because the root folder should not be labeled if (node.depth === 0) { // Add a big padding for the first folder level (the font is bigger than in deeper levels) @@ -218,8 +220,14 @@ function getSquarifiedTreeMap(map: CodeMapNode, state: CcState, mapSizeResolutio DEFAULT_PADDING_FLOOR_LABEL_FROM_LEVEL_1 ) } - if (node.depth > 0 && node.depth < HIERARCHY_LEVELS_WITH_LABLES_UPPER_BOUNDARY) { - return Math.max((rootNode.x1 - rootNode.x0) * DEFAULT_SUB_FLOOR_LABEL_SCALING, DEFAULT_PADDING_FLOOR_LABEL_FROM_LEVEL_2) + if (node.depth > 0) { + const requestedFloorLabelPadding = Math.max( + (rootNode.x1 - rootNode.x0) * DEFAULT_SUB_FLOOR_LABEL_SCALING, + DEFAULT_PADDING_FLOOR_LABEL_FROM_LEVEL_2 + ) + return node.depth < DEFAULT_FLOOR_LABEL_DEPTH + ? requestedFloorLabelPadding + : getFloorLabelPadding(node, requestedFloorLabelPadding, padding) } } @@ -236,6 +244,14 @@ function getSquarifiedTreeMap(map: CodeMapNode, state: CcState, mapSizeResolutio } } +function getFloorLabelPadding(node: HierarchyRectangularNode, requestedPadding: number, fallbackPadding: number) { + const nodeWidth = node.x1 - node.x0 + const maxPaddingKeepingContent = Math.max(0, nodeWidth - MIN_CONTENT_WIDTH_AFTER_LABEL_PADDING) + const maxLabelPadding = Math.min(maxPaddingKeepingContent, nodeWidth * MAX_LABEL_PADDING_SHARE) + + return Math.min(Math.max(fallbackPadding, Math.min(requestedPadding, maxLabelPadding)), maxPaddingKeepingContent) +} + function getEstimatedNodesPerSide(hierarchyNode: HierarchyNode) { let totalNodes = 0 let blacklistedNodes = 0 diff --git a/visualization/app/codeCharta/util/dataMocks.ts b/visualization/app/codeCharta/util/dataMocks.ts index bce177a507..b607118c7d 100644 --- a/visualization/app/codeCharta/util/dataMocks.ts +++ b/visualization/app/codeCharta/util/dataMocks.ts @@ -2186,6 +2186,7 @@ export const STATE: CcState = { isWhiteBackground: false, isColorMetricLinkedToHeightMetric: false, enableFloorLabels: true, + floorLabelDepth: 3, mapColors: { positive: "#69AE40", neutral: "#ddcc00", @@ -2245,6 +2246,7 @@ export const DEFAULT_STATE: CcState = { isWhiteBackground: false, isColorMetricLinkedToHeightMetric: false, enableFloorLabels: true, + floorLabelDepth: 3, mapColors: { base: "#666666", flat: "#AAAAAA",