Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 12 additions & 5 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Cognitive Code Analysis is an approach to understanding and improving code by fo

[Source: Human Cognitive Limitations. Broad, Consistent, Clinical Application of Physiological Principles Will Require Decision Support](https://www.ncbi.nlm.nih.gov/pmc/articles/PMC5822395/)

## Installation
## Installation ⚙️

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

## Documentation
## Documentation 📚

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

## Resources
## Resources 🔗

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

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

## Examples
## Examples 📖

### Cognitive Metrics

Expand Down Expand Up @@ -119,7 +120,13 @@ Class: Doctrine\ORM\Tools\Pagination\Paginator
└───────────────────────────────────────────┴────────┴───────────┴─────────┴───────────┴──────────┴───────┴────────────┴───────────┴────────────┘
```

## License
## Reporting Issues 🪲

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).

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.

## License ⚖️

Copyright Florian Krämer

Expand Down
155 changes: 155 additions & 0 deletions src/Business/Churn/Exporter/SvgTreemapExporter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
<?php

declare(strict_types=1);

namespace Phauthentic\CognitiveCodeAnalysis\Business\Churn\Exporter;

use Phauthentic\CognitiveCodeAnalysis\CognitiveAnalysisException;

/**
* Exports churn data as an SVG treemap.
*
* The size of the rectangles in the treemap is scaled proportionally to the "churn" value of each class.
* Mathematical calculations are delegated to the TreemapMath class, which handles score normalization,
* color mapping, and treemap layout calculations using a slice-and-dice algorithm.
*
* @SuppressWarnings("PHPMD.ShortVariable")
*/
class SvgTreemapExporter implements DataExporterInterface
{
private const SVG_WIDTH = 1200;
private const SVG_HEIGHT = 800;
private const PADDING = 2;

private TreemapMath $treemapMath;

public function __construct()
{
$this->treemapMath = new TreemapMath();
}

/**
* @param array<string, array<string, mixed>> $classes
* @param string $filename
* @throws CognitiveAnalysisException
*/
public function export(array $classes, string $filename): void
{
$svg = $this->generateSvgTreemap(classes: $classes);

if (file_put_contents($filename, $svg) === false) {
throw new CognitiveAnalysisException("Unable to write to file: $filename");
}
}

/**
* Generates a treemap SVG for the churn data.
*
* @param array<string, array<string, mixed>> $classes
* @return string
*/
private function generateSvgTreemap(array $classes): string
{
$items = $this->treemapMath->prepareItems($classes);

[$minScore, $maxScore] = $this->treemapMath->findScoreRange($items);

$rects = $this->treemapMath->calculateTreemapLayout(
items: $items,
x: 0,
y: 0,
width: self::SVG_WIDTH,
height: self::SVG_HEIGHT,
vertical: true,
padding: self::PADDING
);

$svgRects = $this->renderSvgRects(rects: $rects, minScore: $minScore, maxScore: $maxScore);

return $this->wrapSvg(rectsSvg: $svgRects);
}

/**
* Renders SVG rectangles for the treemap.
*
* @param array<int, array<string, mixed>> $rects
* @param float $minScore
* @param float $maxScore
* @return string
*/
private function renderSvgRects(array $rects, float $minScore, float $maxScore): string
{
$svgRects = [];
foreach ($rects as $rect) {
$normalizedScore = $this->treemapMath->normalizeScore(score: $rect['score'], minScore: $minScore, maxScore: $maxScore);
$svgRects[] = $this->renderSvgRect(rect: $rect, normalizedScore: $normalizedScore);
}

return implode("\n", $svgRects);
}

/**
* Renders a single SVG rectangle.
*
* @param array<string, mixed> $rect
* @param float $normalizedScore
* @return string
*/
private function renderSvgRect(array $rect, float $normalizedScore): string
{
$x = $rect['x'] + self::PADDING;
$y = $rect['y'] + self::PADDING;
$width = max(0, $rect['width'] - self::PADDING * 2);
$height = max(0, $rect['height'] - self::PADDING * 2);
$color = $this->treemapMath->scoreToColor(score: $normalizedScore);
$class = htmlspecialchars($rect['class']);
$churn = $rect['churn'];
$score = $rect['score'];
$textX = $x + 4;
$textY = $y + 18;
$label = htmlspecialchars(mb_strimwidth($rect['class'], 0, 40, '…'));

return sprintf(
'<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>',
$x,
$y,
$width,
$height,
$color,
$class,
$churn,
$score,
$textX,
$textY,
$label
);
}

/**
* Wraps SVG rectangles in the SVG document.
*
* @param string $rectsSvg
* @return string
*/
private function wrapSvg(string $rectsSvg): string
{
$width = self::SVG_WIDTH;
$height = self::SVG_HEIGHT;
return <<<SVG
<?xml version="1.0" encoding="UTF-8"?>
<svg width="{$width}" height="{$height}" xmlns="http://www.w3.org/2000/svg">
<style>
text { pointer-events: none; font-family: Arial, sans-serif; }
rect:hover { stroke: #000; stroke-width: 2; }
</style>
<rect x="0" y="0" width="{$width}" height="{$height}" fill="#f8f9fa"/>
<g>
<text x="20" y="30" font-size="28" fill="#333">Churn Treemap</text>
</g>
<g>
{$rectsSvg}
</g>
</svg>
SVG;
}
}
Loading