Skip to content

Commit 3b5a554

Browse files
Adding a Churn Treemap Exporter (#25)
* Experimenting with a Tree-Map for Churn * Adding weighted colors * Adding tests * Fixing PHMD and other issues * Adding named arguments * Updating readme.md * Fixing the SVG Treemap-Test * Adding a doc block to the treemap exporter * Renaming for better understandability * Refactoring the treemap exporter
1 parent 1e7ab92 commit 3b5a554

9 files changed

Lines changed: 491 additions & 15 deletions

File tree

readme.md

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ Cognitive Code Analysis is an approach to understanding and improving code by fo
66
77
[Source: Human Cognitive Limitations. Broad, Consistent, Clinical Application of Physiological Principles Will Require Decision Support](https://www.ncbi.nlm.nih.gov/pmc/articles/PMC5822395/)
88

9-
## Installation
9+
## Installation ⚙️
1010

1111
```bash
1212
composer require --dev phauthentic/cognitive-code-analysis
@@ -42,7 +42,7 @@ Note that this requires a version control system (VCS) to be set up, such as Git
4242
bin/phpcca churn <path-to-folder>
4343
```
4444

45-
## Documentation
45+
## Documentation 📚
4646

4747
* [Cognitive Complexity Analysis](./docs/Cognitive-Complexity-Analysis.md#cognitive-complexity-analysis)
4848
* [Why bother?](./docs/Cognitive-Complexity-Analysis.md#why-bother)
@@ -56,8 +56,9 @@ bin/phpcca churn <path-to-folder>
5656
* [Examples](#examples)
5757
* [Wordpress WP_Debug_Data](#wordpress-wp_debug_data)
5858
* [Doctrine Paginator](#doctrine-paginator)
59+
* [Reporting Issues](#reporting-issues)
5960

60-
## Resources
61+
## Resources 🔗
6162

6263
These pages and papers provide more information on cognitive limitations and readability and the impact on the business.
6364

@@ -72,7 +73,7 @@ These pages and papers provide more information on cognitive limitations and rea
7273
* **Halstead Complexity**
7374
* [Halstead Complexity Measures](https://en.wikipedia.org/wiki/Halstead_complexity_measures)
7475

75-
## Examples
76+
## Examples 📖
7677

7778
### Cognitive Metrics
7879

@@ -119,7 +120,13 @@ Class: Doctrine\ORM\Tools\Pagination\Paginator
119120
└───────────────────────────────────────────┴────────┴───────────┴─────────┴───────────┴──────────┴───────┴────────────┴───────────┴────────────┘
120121
```
121122

122-
## License
123+
## Reporting Issues 🪲
124+
125+
If you find a bug or have a feature request, please open an issue on the [GitHub repository](https://github.com/Phauthentic/cognitive-code-analysis/issues/new).
126+
127+
Especially the AST-parser used under the hood to analyse the code might have issues with certain code constructs, so please provide a minimal example that reproduces the issue.
128+
129+
## License ⚖️
123130

124131
Copyright Florian Krämer
125132

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Phauthentic\CognitiveCodeAnalysis\Business\Churn\Exporter;
6+
7+
use Phauthentic\CognitiveCodeAnalysis\CognitiveAnalysisException;
8+
9+
/**
10+
* Exports churn data as an SVG treemap.
11+
*
12+
* The size of the rectangles in the treemap is scaled proportionally to the "churn" value of each class.
13+
* Mathematical calculations are delegated to the TreemapMath class, which handles score normalization,
14+
* color mapping, and treemap layout calculations using a slice-and-dice algorithm.
15+
*
16+
* @SuppressWarnings("PHPMD.ShortVariable")
17+
*/
18+
class SvgTreemapExporter implements DataExporterInterface
19+
{
20+
private const SVG_WIDTH = 1200;
21+
private const SVG_HEIGHT = 800;
22+
private const PADDING = 2;
23+
24+
private TreemapMath $treemapMath;
25+
26+
public function __construct()
27+
{
28+
$this->treemapMath = new TreemapMath();
29+
}
30+
31+
/**
32+
* @param array<string, array<string, mixed>> $classes
33+
* @param string $filename
34+
* @throws CognitiveAnalysisException
35+
*/
36+
public function export(array $classes, string $filename): void
37+
{
38+
$svg = $this->generateSvgTreemap(classes: $classes);
39+
40+
if (file_put_contents($filename, $svg) === false) {
41+
throw new CognitiveAnalysisException("Unable to write to file: $filename");
42+
}
43+
}
44+
45+
/**
46+
* Generates a treemap SVG for the churn data.
47+
*
48+
* @param array<string, array<string, mixed>> $classes
49+
* @return string
50+
*/
51+
private function generateSvgTreemap(array $classes): string
52+
{
53+
$items = $this->treemapMath->prepareItems($classes);
54+
55+
[$minScore, $maxScore] = $this->treemapMath->findScoreRange($items);
56+
57+
$rects = $this->treemapMath->calculateTreemapLayout(
58+
items: $items,
59+
x: 0,
60+
y: 0,
61+
width: self::SVG_WIDTH,
62+
height: self::SVG_HEIGHT,
63+
vertical: true,
64+
padding: self::PADDING
65+
);
66+
67+
$svgRects = $this->renderSvgRects(rects: $rects, minScore: $minScore, maxScore: $maxScore);
68+
69+
return $this->wrapSvg(rectsSvg: $svgRects);
70+
}
71+
72+
/**
73+
* Renders SVG rectangles for the treemap.
74+
*
75+
* @param array<int, array<string, mixed>> $rects
76+
* @param float $minScore
77+
* @param float $maxScore
78+
* @return string
79+
*/
80+
private function renderSvgRects(array $rects, float $minScore, float $maxScore): string
81+
{
82+
$svgRects = [];
83+
foreach ($rects as $rect) {
84+
$normalizedScore = $this->treemapMath->normalizeScore(score: $rect['score'], minScore: $minScore, maxScore: $maxScore);
85+
$svgRects[] = $this->renderSvgRect(rect: $rect, normalizedScore: $normalizedScore);
86+
}
87+
88+
return implode("\n", $svgRects);
89+
}
90+
91+
/**
92+
* Renders a single SVG rectangle.
93+
*
94+
* @param array<string, mixed> $rect
95+
* @param float $normalizedScore
96+
* @return string
97+
*/
98+
private function renderSvgRect(array $rect, float $normalizedScore): string
99+
{
100+
$x = $rect['x'] + self::PADDING;
101+
$y = $rect['y'] + self::PADDING;
102+
$width = max(0, $rect['width'] - self::PADDING * 2);
103+
$height = max(0, $rect['height'] - self::PADDING * 2);
104+
$color = $this->treemapMath->scoreToColor(score: $normalizedScore);
105+
$class = htmlspecialchars($rect['class']);
106+
$churn = $rect['churn'];
107+
$score = $rect['score'];
108+
$textX = $x + 4;
109+
$textY = $y + 18;
110+
$label = htmlspecialchars(mb_strimwidth($rect['class'], 0, 40, ''));
111+
112+
return sprintf(
113+
'<g><rect x="%.2f" y="%.2f" width="%.2f" height="%.2f" fill="%s" stroke="#222" stroke-width="1"/><title>%s&#10;Churn: %s&#10;Score: %s</title><text x="%.2f" y="%.2f" font-size="13" fill="#000">%s</text></g>',
114+
$x,
115+
$y,
116+
$width,
117+
$height,
118+
$color,
119+
$class,
120+
$churn,
121+
$score,
122+
$textX,
123+
$textY,
124+
$label
125+
);
126+
}
127+
128+
/**
129+
* Wraps SVG rectangles in the SVG document.
130+
*
131+
* @param string $rectsSvg
132+
* @return string
133+
*/
134+
private function wrapSvg(string $rectsSvg): string
135+
{
136+
$width = self::SVG_WIDTH;
137+
$height = self::SVG_HEIGHT;
138+
return <<<SVG
139+
<?xml version="1.0" encoding="UTF-8"?>
140+
<svg width="{$width}" height="{$height}" xmlns="http://www.w3.org/2000/svg">
141+
<style>
142+
text { pointer-events: none; font-family: Arial, sans-serif; }
143+
rect:hover { stroke: #000; stroke-width: 2; }
144+
</style>
145+
<rect x="0" y="0" width="{$width}" height="{$height}" fill="#f8f9fa"/>
146+
<g>
147+
<text x="20" y="30" font-size="28" fill="#333">Churn Treemap</text>
148+
</g>
149+
<g>
150+
{$rectsSvg}
151+
</g>
152+
</svg>
153+
SVG;
154+
}
155+
}

0 commit comments

Comments
 (0)