Skip to content

Commit 0bc9281

Browse files
Renders bubble chart server-side
Renders the bubble chart using server-side data, removing the need for file uploads. This change streamlines the bubble chart generation process by directly injecting the dependency data into the view. It eliminates the file upload step, simplifying the user experience and improving performance. Also refactors view to blade template and includes inline js.
1 parent 7173b6b commit 0bc9281

File tree

6 files changed

+228
-21
lines changed

6 files changed

+228
-21
lines changed

app/Presenter/Analyze/Class/Bubble/BubbleView.php

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,30 @@
22

33
namespace App\Presenter\Analyze\Class\Bubble;
44

5+
use Illuminate\View\Factory as View;
6+
use App\Presenter\Analyze\Shared\Views\SystemFileLauncher;
7+
58
class BubbleView
69
{
10+
public function __construct(
11+
private readonly View $view,
12+
private readonly SystemFileLauncher $systemFileLauncher,
13+
) {}
14+
715
public function show(BubbleViewModel $viewModel): void
816
{
9-
dd($viewModel->dependencies());
17+
$html = $this->render($viewModel);
18+
19+
$this->systemFileLauncher->save($html);
20+
$this->systemFileLauncher->open();
21+
}
22+
23+
private function render(BubbleViewModel $viewModel): string
24+
{
25+
$view = $this->view->make('bubble-page', [
26+
'dependencies' => $viewModel->dependencies(),
27+
]);
28+
29+
return $view->render();
1030
}
1131
}

app/Presenter/Analyze/Class/Bubble/BubbleViewModel.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@
55
class BubbleViewModel
66
{
77
public function __construct(
8-
public readonly array $foldersData,
8+
public readonly array $dependencies,
99
) {}
10+
11+
public function dependencies(): array
12+
{
13+
return $this->dependencies;
14+
}
1015
}
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
<!DOCTYPE html>
2+
<html lang="fr">
3+
4+
<head>
5+
<meta charset="UTF-8">
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
7+
<title>Cartographie des dépendances par dossier</title>
8+
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.8.5/d3.min.js"></script>
9+
<style>
10+
.container { max-width: 1800px; margin: 0 auto; padding: 16px; font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
11+
#chart { height: 800px; min-height: 800px; }
12+
.legend { max-height: 180px; overflow: auto; }
13+
.chart-title { margin-bottom: 8px; font-weight: 600; }
14+
.chart-btn { font-size: 13px; }
15+
.dep-link { pointer-events: none; }
16+
.bubble.highlight circle { stroke: #111827; stroke-width: 3; opacity: 1; }
17+
.bubble.adjacent circle { stroke: #2563eb; stroke-width: 2; opacity: 0.95; }
18+
.bubble.dimmed { opacity: 0.25; }
19+
.dep-link.highlight { stroke-width: 3 !important; opacity: 1 !important; }
20+
.dep-link.dimmed { opacity: 0.15 !important; }
21+
.header h1 { margin: 0 0 4px; font-size: 24px; }
22+
.header p { margin: 0; color: #4b5563; font-size: 14px; }
23+
.main-content { display: flex; gap: 16px; margin-top: 16px; }
24+
.controls { width: 320px; flex-shrink: 0; }
25+
.chart-container { flex: 1; min-width: 0; }
26+
.form-section { background: #f9fafb; border-radius: 8px; padding: 12px 14px; border: 1px solid #e5e7eb; }
27+
.form-section h3 { margin: 0 0 8px; font-size: 14px; }
28+
.form-group { display: flex; flex-direction: column; gap: 8px; }
29+
.input-group { display: flex; flex-direction: column; gap: 4px; font-size: 13px; }
30+
.input-group label { font-weight: 500; color: #374151; }
31+
.input-group input[type="text"], .input-group select { padding: 6px 8px; border-radius: 4px; border: 1px solid #d1d5db; font-size: 13px; }
32+
.btn { margin-top: 8px; padding: 6px 10px; font-size: 13px; border-radius: 4px; border: none; background: #2563eb; color: white; cursor: pointer; }
33+
.btn:hover { background: #1d4ed8; }
34+
.status { margin-top: 8px; font-size: 12px; display: none; }
35+
.status.success { color: #15803d; }
36+
.status.error { color: #b91c1c; }
37+
.status.info { color: #1d4ed8; }
38+
.chart-controls { display: none; align-items: center; gap: 6px; margin-bottom: 6px; font-size: 13px; }
39+
.loading { display: none; align-items: center; gap: 8px; font-size: 13px; color: #4b5563; }
40+
.spinner { width: 16px; height: 16px; border-radius: 999px; border: 2px solid #e5e7eb; border-top-color: #2563eb; animation: spin 0.6s linear infinite; }
41+
.empty-state { text-align: center; color: #6b7280; font-size: 14px; padding: 32px 16px; }
42+
.empty-state svg { width: 40px; height: 40px; margin-bottom: 8px; color: #9ca3af; }
43+
.tooltip { position: absolute; pointer-events: none; background: rgba(17, 24, 39, 0.9); color: white; padding: 6px 8px; border-radius: 4px; font-size: 11px; max-width: 260px; z-index: 20; }
44+
.legend { margin-top: 8px; display: flex; flex-wrap: wrap; gap: 4px 10px; font-size: 11px; color: #374151; }
45+
.legend-item { display: inline-flex; align-items: center; gap: 4px; white-space: nowrap; }
46+
.legend-color { width: 10px; height: 10px; border-radius: 999px; border: 1px solid rgba(0,0,0,0.15); }
47+
@keyframes spin { to { transform: rotate(360deg); } }
48+
</style>
49+
</head>
50+
51+
<body>
52+
<div class="container">
53+
<div class="header">
54+
<h1>Cartographie des dépendances par dossier</h1>
55+
<p>Vue en bulles des relations entre dossiers de premier niveau</p>
56+
</div>
57+
58+
<div class="main-content">
59+
<div class="controls">
60+
<div class="form-section">
61+
<h3>Configuration</h3>
62+
<div class="form-group">
63+
<div class="input-group">
64+
<label for="parentFolder">Dossier parent (optionnel) :</label>
65+
<input type="text" id="parentFolder" placeholder="Ex: app/Application">
66+
</div>
67+
<div class="input-group">
68+
<label for="metricSelect">Taille des bulles basée sur :</label>
69+
<select id="metricSelect">
70+
<option value="total">Total (classes+interfaces+abstracts)</option>
71+
<option value="constant">Taille standard</option>
72+
<option value="classes">Nombre de classes</option>
73+
<option value="interfaces">Nombre d'interfaces</option>
74+
<option value="abstracts">Nombre d'abstraites</option>
75+
<option value="efferent_coupling">Efferent Coupling (Σ)</option>
76+
<option value="afferent_coupling">Afferent Coupling (Σ)</option>
77+
<option value="instability_avg">Instabilité (moyenne)</option>
78+
<option value="loc_total">Lignes de code (Σ)</option>
79+
<option value="ccn_total">Complexité cyclomatique (Σ)</option>
80+
</select>
81+
</div>
82+
<div class="input-group">
83+
<label for="colorSelect">Couleur des bulles basée sur :</label>
84+
<select id="colorSelect">
85+
<option value="group" selected>Par dossier (palette)</option>
86+
<option value="instability_avg">Instabilité (moyenne)</option>
87+
</select>
88+
</div>
89+
<div class="input-group">
90+
<label>Profondeur des dossiers :</label>
91+
<div id="depthRadios" style="display: inline-flex; gap: 10px; align-items: center;">
92+
<label style="display: inline-flex; align-items: center; gap: 4px;">
93+
<input type="radio" name="depthSelect" value="1" checked>
94+
1
95+
</label>
96+
<label style="display: inline-flex; align-items: center; gap: 4px;">
97+
<input type="radio" name="depthSelect" value="2">
98+
2
99+
</label>
100+
<label style="display: inline-flex; align-items: center; gap: 4px;">
101+
<input type="radio" name="depthSelect" value="3">
102+
3
103+
</label>
104+
<label style="display: inline-flex; align-items: center; gap: 4px;">
105+
<input type="radio" name="depthSelect" value="4">
106+
4
107+
</label>
108+
</div>
109+
</div>
110+
<button class="btn" id="analyzeBtn">Générer</button>
111+
</div>
112+
<div class="status" id="analysisStatus"></div>
113+
</div>
114+
</div>
115+
116+
<div class="chart-container">
117+
<div class="chart-controls" id="chartControls" style="display: none;">
118+
<button class="chart-btn" id="resetZoomBtn" title="Réinitialiser le zoom">🔍 Reset</button>
119+
<button class="chart-btn" id="resetFiltersBtn" title="Réafficher toutes les bulles supprimées">🧹 Filtres</button>
120+
<button class="chart-btn" id="downloadBtn" title="Télécharger en PNG">📥 PNG</button>
121+
<button class="chart-btn" id="downloadSvgBtn" title="Télécharger en SVG">📥 SVG</button>
122+
<label style="margin-left: 12px; display: inline-flex; align-items: center; gap: 6px; font-size: 13px;">
123+
Type dependance:
124+
<select id="cycleModeSelect" style="padding: 4px 8px; font-size: 13px;">
125+
<option value="all" selected>Toutes</option>
126+
<option value="weak">Dépendances faibles uniquement</option>
127+
<option value="direct">Cycles directs (A↔B)</option>
128+
<option value="multi">Cycles multi (A→B→C→A)</option>
129+
</select>
130+
</label>
131+
</div>
132+
133+
<div class="loading" id="loading">
134+
<div class="spinner"></div>
135+
<p>Analyse en cours...</p>
136+
</div>
137+
138+
<div class="empty-state" id="emptyState">
139+
<svg viewBox="0 0 24 24" fill="currentColor">
140+
<path d="M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M18,20H6V4H13V9H18V20Z" />
141+
</svg>
142+
<h3>Données d'analyse fournies</h3>
143+
<p>Les données JSON d'analyse (dossiers → relations) sont fournies automatiquement par l'outil de ligne de commande.</p>
144+
</div>
145+
146+
<div class="chart-title" id="chartTitle" style="display: none;"></div>
147+
<svg id="chart" style="display: none;"></svg>
148+
<div class="legend" id="legend" style="display: none;"></div>
149+
</div>
150+
</div>
151+
</div>
152+
153+
<div class="tooltip" id="tooltip" style="opacity: 0;"></div>
154+
155+
<script>
156+
// Données injectées par le CLI
157+
window.bubbleData = @json($dependencies);
158+
</script>
159+
<script>
160+
{!! file_get_contents(base_path('resources/views/bubble/js/data-processor.js')) !!}
161+
</script>
162+
<script>
163+
{!! file_get_contents(base_path('resources/views/bubble/js/chart-generator.js')) !!}
164+
</script>
165+
<script>
166+
{!! file_get_contents(base_path('resources/views/bubble/js/download-handler.js')) !!}
167+
</script>
168+
<script>
169+
{!! file_get_contents(base_path('resources/views/bubble/js/main.js')) !!}
170+
</script>
171+
</body>
172+
173+
</html>
174+
175+

resources/views/bubble/index.html

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -45,19 +45,6 @@ <h1>Cartographie des dépendances par dossier</h1>
4545

4646
<div class="main-content">
4747
<div class="controls">
48-
<div class="form-section">
49-
<h3>Chargement du fichier d'analyse</h3>
50-
<div class="form-group">
51-
<div class="input-group">
52-
<label for="jsonFile">Fichier JSON :</label>
53-
<input type="file" id="jsonFile" accept=".json">
54-
<div class="file-info" id="fileInfo"></div>
55-
</div>
56-
57-
</div>
58-
<div class="status" id="fileStatus"></div>
59-
</div>
60-
6148
<div class="form-section">
6249
<h3>Configuration</h3>
6350
<div class="form-group">
@@ -140,8 +127,8 @@ <h3>Configuration</h3>
140127
<svg viewBox="0 0 24 24" fill="currentColor">
141128
<path d="M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M18,20H6V4H13V9H18V20Z" />
142129
</svg>
143-
<h3>Aucun fichier chargé</h3>
144-
<p>Veuillez charger un fichier JSON d'analyse (dossiers → relations) pour commencer</p>
130+
<h3>Données d'analyse fournies</h3>
131+
<p>Les données JSON d'analyse (dossiers → relations) sont fournies automatiquement par l'outil de ligne de commande.</p>
145132
</div>
146133

147134
<div class="chart-title" id="chartTitle" style="display: none;"></div>

resources/views/bubble/js/main.js

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,22 @@ function generateChart() {
4848
}
4949

5050
// Init
51-
document.addEventListener('DOMContentLoaded', function () {
52-
const jsonFileInput = document.getElementById('jsonFile');
51+
document.addEventListener('DOMContentLoaded', function () {
52+
// Si les données JSON sont déjà fournies par l'outil (ex: window.bubbleData),
53+
// on les utilise directement et on génère le graphique sans formulaire.
54+
if (window.bubbleData) {
55+
topLevelData = window.bubbleData;
56+
const emptyState = document.getElementById('emptyState');
57+
if (emptyState) {
58+
emptyState.style.display = 'none';
59+
}
60+
generateChart();
61+
}
62+
5363
const analyzeBtn = document.getElementById('analyzeBtn');
54-
jsonFileInput.addEventListener('change', handleFileUpload);
55-
analyzeBtn.addEventListener('click', generateChart);
64+
if (analyzeBtn) {
65+
analyzeBtn.addEventListener('click', generateChart);
66+
}
5667
const cycleModeSelect = document.getElementById('cycleModeSelect');
5768
if (cycleModeSelect) {
5869
cycleModeSelect.addEventListener('change', generateChart);

tests/Builders/AnalyzeMetricBuilder.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ class AnalyzeMetricBuilder
99
private string $name = 'default';
1010
private array $dependencies = [];
1111
private bool $abstract = false;
12+
private bool $isInterface = false;
1213
private float $efferent = 0;
1314
private float $afferent = 0;
1415
private float $instability = 0;
@@ -29,6 +30,13 @@ public function isAbstract(): self
2930
return $this;
3031
}
3132

33+
public function isInterface(): self
34+
{
35+
$this->isInterface = true;
36+
37+
return $this;
38+
}
39+
3240
public function withDependencies(array $value): self
3341
{
3442
$this->dependencies = $value;
@@ -56,6 +64,7 @@ public function build(): AnalyzeMetric
5664
'name' => $this->name,
5765
'dependencies' => $this->dependencies,
5866
'abstract' => $this->abstract,
67+
'isInterface' => $this->isInterface,
5968
'coupling' => [
6069
'efferent' => $this->efferent,
6170
'afferent' => $this->afferent,

0 commit comments

Comments
 (0)