Skip to content

Commit bfc6cd6

Browse files
committed
feat: add treemap for hotspots
1 parent 6ed670a commit bfc6cd6

13 files changed

Lines changed: 289 additions & 17 deletions

File tree

.detective/config.json

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,32 @@
44
"apps/backend/src/options",
55
"apps/backend/src/services",
66
"apps/backend/src/utils",
7-
"apps/backend/src/infrastructure"
7+
"apps/backend/src/infrastructure",
8+
"apps/frontend/src/app/features/coupling",
9+
"apps/frontend/src/app/features/hotspot",
10+
"apps/frontend/src/app/features/team-alignment",
11+
"apps/frontend/src/app/shell/about",
12+
"apps/frontend/src/app/shell/filter-tree",
13+
"apps/frontend/src/app/shell/nav",
14+
"apps/frontend/src/app/model",
15+
"apps/frontend/src/app/ui/doughnut",
16+
"apps/frontend/src/app/ui/graph",
17+
"apps/frontend/src/app/ui/limits",
18+
"apps/frontend/src/app/ui/loading",
19+
"apps/frontend/src/app/ui/resizer",
20+
"apps/frontend/src/app/ui/treemap"
21+
],
22+
"groups": [
23+
"apps/backend/src",
24+
"apps/backend",
25+
"apps/frontend/src/app/features",
26+
"apps/frontend/src/app/shell",
27+
"apps/frontend/src/app/ui",
28+
"apps/frontend/src/app",
29+
"apps/frontend/src",
30+
"apps/frontend",
31+
"apps"
832
],
9-
"groups": ["apps/backend/src", "apps/backend", "apps"],
1033
"entries": [],
1134
"filter": {
1235
"files": [],

.detective/hash

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
cc20f4a7cf2cd362d1f174f556b432a20bc7fb95, v1.1.6
1+
503c63bcf7fab325fc9687a40ad048b2b16ca636, v1.1.6

.detective/log

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
"Manfred Steyer <manfred.steyer@gmx.net>,Tue Oct 8 17:44:47 2024 +0200 6ed670acc998f83279713bc9c2a16ef7a79f66a1,feat: filter hotspots using slider and percent"
2+
1 1 .detective/hash
3+
33 3 .detective/log
4+
70 11 apps/backend/src/services/hotspot.ts
5+
8 10 apps/frontend/src/app/features/hotspot/hotspot.component.html
6+
2 0 apps/frontend/src/app/features/hotspot/hotspot.component.ts
7+
1 1 apps/frontend/src/app/features/hotspot/hotspot.store.ts
8+
19
"Manfred Steyer <manfred.steyer@gmx.net>,Sun Sep 29 12:41:39 2024 +0200 4ee102065cca1a50063a871b078ec87680116efb,chore(release): publish 1.1.6"
210
10 0 CHANGELOG.md
311
1 1 apps/backend/package.json

apps/backend/src/services/hotspot.ts

Lines changed: 39 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -41,14 +41,17 @@ export type AggregatedHotspot = {
4141
parent: string;
4242
module: string;
4343
count: number;
44-
countBelow: number;
44+
countWarning: number;
45+
countHotspot: number;
46+
countOk: number;
4547
};
4648

4749
export type AggregatedHotspotsResult = {
4850
aggregated: AggregatedHotspot[];
4951
minScore: number;
5052
maxScore: number;
51-
boundary: number;
53+
warningBoundary: number;
54+
hotspotBoundary: number;
5255
};
5356

5457
type Stats = {
@@ -104,41 +107,64 @@ export async function aggregateHotspots(
104107

105108
const stats = collectStats(modules, hotspots);
106109

107-
const boundary = stats.maxScore * (criteria.minScore / 100);
108-
const result = aggregateStats(modules, stats, boundary);
110+
const warningBoundary = stats.maxScore * (criteria.minScore / 100);
111+
const hotspotBoundary =
112+
warningBoundary + (stats.maxScore - warningBoundary) / 2;
113+
114+
const result = aggregateStats(
115+
modules,
116+
stats,
117+
warningBoundary,
118+
hotspotBoundary
119+
);
109120

110121
result.sort((a, b) => b.count - a.count);
111122

112123
return {
113124
aggregated: result,
114125
maxScore: stats.maxScore,
115126
minScore: stats.minScore,
116-
boundary,
127+
hotspotBoundary,
128+
warningBoundary,
117129
};
118130
}
119131

120132
function aggregateStats(
121133
modules: string[],
122134
stats: Stats,
123-
boundary: number
135+
warningBoundary: number,
136+
hotspotBoundary: number
124137
): AggregatedHotspot[] {
125138
const result: AggregatedHotspot[] = [];
126139
for (const module of modules) {
127140
const moduleStats = stats.scores.get(module);
128-
const count = moduleStats.reduce(
129-
(acc, v) => (v > boundary ? acc + 1 : acc),
130-
0
131-
);
132-
const countBelow = moduleStats.length - count;
141+
142+
let countWarning = 0;
143+
let countHotspot = 0;
144+
let countOk = 0;
145+
146+
for (const stat of moduleStats) {
147+
if (stat > hotspotBoundary) {
148+
countHotspot++;
149+
} else if (stat < warningBoundary) {
150+
countOk++;
151+
} else {
152+
countWarning++;
153+
}
154+
}
155+
156+
// const countBelow = moduleStats.length - count;
133157

134158
const displayFolder = toDisplayFolder(module);
135159
const parent = path.dirname(displayFolder);
136160

137161
result.push({
138162
parent,
139163
module: displayFolder,
140-
count,
141-
countBelow,
164+
count: countOk,
165+
countOk,
166+
countWarning,
167+
countHotspot,
142168
});
143169
}
144170
return result;
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import 'chartjs-chart-treemap';
2+
3+
import { ChartEvent, InteractionItem } from 'chart.js';
4+
5+
import { AggregatedHotspot } from '../../model/hotspot-result';
6+
import { TreeMapChartConfig } from '../../ui/treemap/treemap.component';
7+
8+
type HotspotDataSet = {
9+
tree: AggregatedHotspot[];
10+
};
11+
12+
export function toTreeMapConfig(
13+
aggregated: AggregatedHotspot[]
14+
): TreeMapChartConfig {
15+
const values = aggregated.flatMap((v) => [
16+
{ ...v, count: v.countHotspot, type: 'hotspot' },
17+
{ ...v, count: v.countWarning, type: 'warning' },
18+
{ ...v, count: v.countOk, type: 'fine' },
19+
]);
20+
21+
const options = {
22+
onClick: (_event: ChartEvent, elements: InteractionItem[]) => {
23+
if (elements.length > 0) {
24+
const element = elements[elements.length - 1];
25+
const dataIndex = element.index;
26+
const dataset = config.data.datasets[0] as unknown as HotspotDataSet;
27+
const tree = dataset.tree;
28+
const item = tree[dataIndex];
29+
console.log('item', item);
30+
}
31+
},
32+
onHover: (event: ChartEvent, elements: InteractionItem[]) => {
33+
const chartElement = event.native?.target as HTMLCanvasElement;
34+
if (elements.length >= 2) {
35+
chartElement.style.cursor = 'pointer';
36+
} else {
37+
chartElement.style.cursor = 'default';
38+
}
39+
},
40+
plugins: {
41+
title: {
42+
display: true,
43+
text: 'Hotspots',
44+
},
45+
legend: {
46+
display: false,
47+
},
48+
tooltip: {
49+
callbacks: {
50+
title() {
51+
return 'File Count';
52+
},
53+
},
54+
},
55+
},
56+
};
57+
58+
const config: TreeMapChartConfig = {
59+
type: 'treemap',
60+
data: {
61+
datasets: [
62+
{
63+
data: values,
64+
key: 'count',
65+
groups: ['parent', 'module', 'type'],
66+
spacing: 1,
67+
borderWidth: 0.5,
68+
borderColor: '#EFEFEF',
69+
// backgroundColor: 'rgba(220,230,220,0.3)',
70+
backgroundColor: (ctx) => {
71+
if (typeof ctx.raw?.l !== 'undefined' && ctx.raw?.l < 2) {
72+
return '#EFEFEF';
73+
}
74+
75+
switch (ctx.raw?.g) {
76+
case 'hotspot':
77+
return '#E74C3C';
78+
case 'warning':
79+
return '#F1C40F';
80+
case 'fine':
81+
return '#2ECC71';
82+
}
83+
84+
return 'gray';
85+
},
86+
// hoverBackgroundColor: 'rgba(220,230,220,0.5)',
87+
captions: {
88+
align: 'center',
89+
display: true,
90+
color: 'black',
91+
font: {
92+
size: 14,
93+
},
94+
hoverFont: {
95+
size: 16,
96+
weight: 'bold',
97+
},
98+
padding: 5,
99+
},
100+
labels: {
101+
display: false,
102+
overflow: 'hidden',
103+
},
104+
},
105+
],
106+
},
107+
options: options,
108+
};
109+
110+
return config;
111+
}

apps/frontend/src/app/features/hotspot/hotspot.component.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,3 +115,5 @@
115115
}
116116
</div>
117117
</div>
118+
119+
<app-treemap [chartConfig]="treeMapConfig()"></app-treemap>

apps/frontend/src/app/features/hotspot/hotspot.component.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,13 @@ import {
2929
} from '../../model/hotspot-result';
3030
import { Limits } from '../../model/limits';
3131
import { LimitsComponent } from '../../ui/limits/limits.component';
32+
import { TreeMapComponent } from '../../ui/treemap/treemap.component';
3233
import { debounceTimeSkipFirst } from '../../utils/debounce';
3334
import { EventService } from '../../utils/event.service';
3435
import { lastSegments } from '../../utils/segments';
3536
import { mirror } from '../../utils/signal-helpers';
3637

38+
import { toTreeMapConfig } from './hotspot-adapter';
3739
import { HotspotStore } from './hotspot.store';
3840

3941
interface Option {
@@ -57,6 +59,7 @@ interface Option {
5759
FormsModule,
5860
MatIconModule,
5961
MatTooltipModule,
62+
TreeMapComponent,
6063
],
6164
templateUrl: './hotspot.component.html',
6265
styleUrl: './hotspot.component.css',
@@ -97,6 +100,8 @@ export class HotspotComponent {
97100
formatAggregated(this.aggregatedResult().aggregated)
98101
);
99102

103+
treeMapConfig = computed(() => toTreeMapConfig(this.formattedAggregated()));
104+
100105
formattedHotspots = computed(() =>
101106
formatHotspots(
102107
this.hotspotResult().hotspots,
@@ -162,7 +167,7 @@ export class HotspotComponent {
162167
function formatAggregated(hotspot: AggregatedHotspot[]): AggregatedHotspot[] {
163168
return hotspot.map((hs) => ({
164169
...hs,
165-
module: lastSegments(hs.module, 3),
170+
module: lastSegments(hs.module, 1),
166171
}));
167172
}
168173

apps/frontend/src/app/model/hotspot-result.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,26 @@ export interface HotspotCriteria {
3030
}
3131

3232
export interface AggregatedHotspot {
33+
parent: string;
3334
module: string;
3435
count: number;
36+
countWarning: number;
37+
countHotspot: number;
38+
countOk: number;
3539
}
3640

3741
export interface AggregatedHotspotsResult {
3842
aggregated: AggregatedHotspot[];
43+
maxScore: number;
44+
minScore: number;
45+
warningBoundary: number;
46+
hotspotBoundary: number;
3947
}
4048

4149
export const initAggregatedHotspotsResult: AggregatedHotspotsResult = {
4250
aggregated: [],
51+
maxScore: 0,
52+
minScore: 0,
53+
warningBoundary: 0,
54+
hotspotBoundary: 0,
4355
};

apps/frontend/src/app/ui/treemap/treemap.component.css

Whitespace-only changes.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<div class="tm-container">
2+
<canvas #canvas class="tm-diagram"></canvas>
3+
</div>

0 commit comments

Comments
 (0)